Agent 查询数据库时的上下文管理:查询结果返回 10 万行,怎么在有限 Token 内处理

我最早做数据库 Agent 的时候,天真地以为把 SQL 查询结果全塞进上下文,模型就能替我做报表。直到第一次生产调用返回 400 错误:请求超限。10 万行结果,一夜回到原始时代。

AI technology illustration

后来我明白了三件事:Token 有限是硬约束;模型读长文本会丢中间信息;真正的解法不是加大窗口,而是减少进入上下文的东西。这篇文章讲讲我处理这类问题的完整思路。

先算一笔账:10 万行要多少 Token?

假设每行 100 个字符(一条订单记录差不多),10 万行就是 1000 万字符。按 OpenAI 的大致换算,4 个字符约等于 1 个 Token,因此 10 万行约等于 250 万 Token。而 GPT-4 目前最大的上下文窗口也就 128k,差了近 20 倍。

数据规模 字符数(估算) Token 数(估算) 128k 窗口能放下吗
100 行 1 万 ~2.5k
1 万行 100 万 ~250k 不能
10 万行 1000 万 ~2.5M 完全不能

所以第一步很简单:物理上装不下。哪怕用 Gemini 的 2M 上下文,也要被占掉 80% 以上的空间,留给指令和工具调用的空间少得可怜。

就算塞得下,模型也读不进去

你可能会反驳:那我用长上下文模型不就行了?这里有个更隐蔽的坑。UC Berkeley 的论文 Lost in the Middle 做了一组实验:把答案放在上下文的不同位置,然后看模型准确率。结果是一条 U 型曲线——两端高,中间崩。当关键信息位于整个上下文约 50% 的位置时,模型准确率最低,几乎只有两端的一半。

这意味着什么?哪怕未来窗口无限大,模型依然倾向于“记头记尾”,中间的数万行数据会变成干扰信息。你可能会想:模型不是有注意力机制吗?但它决定注意哪里,靠的是位置相关性,而不是你心里的重要性排序。这不是个例。论文在多个模型和多个问答任务上都观察到了同样的 U 型曲线,所以别指望换个模型就能解决。

让数据库先把答案算出来

模型最擅长的是自然语言,不是算术。你问 Agent“每个分类的平均订单金额是多少”,如果它把 10 万行明细全拉回来,那不是在分析数据,那是在自我折磨。

正确做法:先让 Agent 判断问题需要的粒度,然后生成聚合 SQL。

-- 不推荐:拉回 10 万行明细
SELECT * FROM orders WHERE order_date >= '2024-01-01';

-- 推荐:直接在数据库里聚合
SELECT category, COUNT(*), AVG(amount)
FROM orders
WHERE order_date >= '2024-01-01'
GROUP BY category;

注意,这个决策不能事后补救。Agent 在写 SQL 之前就要想清楚:用户的问题是否能用聚合结果回答。如果能,就绝不 SELECT *。这是最重要的一步,它把 10 万行压缩成了几十个统计值。

LangChain 的 SQL Agent 文档里也强调,要让 Agent 先生成一个“最小化”的查询,只在必要时才拉明细。说白了,你是在教模型“用数据库算账,而不是用上下文数数”。

采样是探索,不是结论

不是所有问题都能提前知道聚合粒度。比如用户问“这张表里数据大概长什么样?”这时候你不需要全量数据,只需要 LIMIT 10。

preview = conn.execute('''
    SELECT * FROM orders ORDER BY order_date DESC LIMIT 10
''').fetchall()
# 让模型基于 preview 理解表结构,再决定下一步问什么

但采样有个致命点:它只能给模型一个“感觉”,不能给出精确答案。如果你问“平均金额是多少”,采样回答的误差可能大得离谱。所以我把它定位为“探索”工具,用于生成后续 SQL,而不是最终结果。

用统计摘要代替原始行

当用户确实需要全量计算时,与其传原始行,不如在程序里先算好统计摘要,然后把摘要交给模型。这个思路在做数据分析时尤其有效。

举个例子,你想让 Agent 汇报订单金额分布。你可以用 pandas 或 SQL 提前计算 min、max、median、分位数,然后传这样一个小 JSON:

{
  "count": 100000,
  "min": 1.5,
  "max": 9999.0,
  "mean": 128.3,
  "p50": 78.0,
  "p95": 356.0
}

这比 10 万行原始数据的信息密度高得多。看一下对比:

输入类型 Token 占用 能回答什么 丢掉了什么
原始明细 250 万 理论上一切问题 窗口装不下,中间信息模型读不到
统计摘要 几百 分布、极值、平均值、分位数 个体细节、随机回溯能力
分组直方图 几千 分段统计、趋势判断 精确到每行的所有信息

于是你的 Agent 从一个“读不完书的书生”变成了“拿着账本的会计”。

分块 + 递归综合

如果用户的问题必须基于全量明细,比如“哪些订单比较异常?”这种无法用聚合回答的问题,我们只能分块处理。思路类似 MapReduce:

  1. 把 10 万行按时间或主键切成 N 块,确保每块落在 10k Token 以内。
  2. 对每一块单独调用一次模型,让它输出该块的异常模式、关键统计和可能的问题点。
  3. 把所有块的摘要汇总成一次新的上下文,让主模型做交叉比对和最终判断。

分块大小可以这样估算:10k Token 约等于 4 万个字符,按每行 100 字符算,就是 400 行一块。10 万行大概切成 250 块。如果你的模型窗口更大,可以切大点,但记得 Lost in the Middle 的教训,建议每块不超过窗口的四分之一。

注意,每一块的输出必须是结构化的,比如固定格式的 JSON,否则后面无法合并。LangChain 的 map-reduce summarization 也用了类似思想,但在 SQL 场景下,我建议你自己写这个流程,因为你需要每一步都可审计。

我踩过的坑:盲目调大窗口

有一段时间我心想,既然上下文窗口不够,那就直接调大。于是我切换到了支持长上下文的模型,还把重试参数调到最大。结果呢?成本涨了十几倍,而且模型在长上下文中开始“抛锚”——明明答案就在中间一段,它就是引用错数字。当时我以为 128k 的窗口够用,但 1 万行数据就占了 120k,剩下的空间只剩几千 Token,模型连记住指令都费劲。

后来我看了 Pinecone 的 Context Engineering 这篇文章,才想通:上下文管理不是“给模型看更多”,而是“给模型看最相关”。模型不需要见过所有数据,它需要见过最关键的数据。从那以后,我把重心从“怎么塞进去”转向了“怎么减少要见的量”。

最后的判断:模型不是数据仓库

写到这里,结论已经清晰了。处理 10 万行查询结果的正确姿势,不是把它喂给模型,而是在接近数据库的地方把它消化掉。你的 Agent 应该像一位分析师:先构思分析框架,再写精确的 SQL,最后只带走结论。而数据库,永远是那个强大的计算引擎。

我仍然不推荐把 Agent 当成数据存储。即便未来出现 1 亿 Token 的上下文,把十亿行数据塞进去也是荒谬的。除非模型的注意力机制有根本性突破,否则“上下文压缩”和“层级过滤”才是数据库 Agent 的核心技能。

以上是我这段实践里最大的认知跳跃。希望对你有用。

原创文章,作者:guanweilu,如若转载,请注明出处:https://guanweilu.cn/article/613.html

(0)
上一篇 1天前
下一篇 1天前

相关推荐