我给 AI Agent 写了个查数据库的工具,某次它查出了几万行结果原封不动塞进了对话上下文,当场超出 token 上限报错,就算没报错模型也被海量数据淹没得抓不住重点:一次工具返回过大塞爆上下文、误以为给模型的信息越多越好的深度复盘
那次 Agent 的"翻车",让我重新理解了"给模型喂数据"这件事。我在做一个 AI Agent,给它配了个工具 query_database(sql),让它能查业务数据来回答问题。平时挺好用,直到某次用户问了个"把所有订单列出来分析一下"之类的问题,Agent 生成了个没加 LIMIT 的查询,工具查出了几万行数据,然后我把这几万行原封不动地拼成字符串、塞回了对话上下文。结果:当场 context length exceeded——超出了模型的 token 上限,直接报错,整个对话崩了。我把返回截断到"勉强不超限"后再试,发现就算不报错,问题依然严重:模型被那几千行原始数据淹没,抓不住重点、回答得又慢又贵又含糊,还经常忽略掉用户真正关心的那一点。复盘这件事,我才真正想明白,后背发凉:问题出在我有一个根深蒂固的错觉——"给模型的信息越多越全越好"。我以为"把查到的全部数据都给模型,它就能更好地分析",于是把工具的原始全量返回直接灌了进去;可我忽略了两件事:①上下文窗口是有限的(有 token 上限),几万行数据根本塞不下、直接撑爆;②即便塞得下,LLM 的"有效注意力"也是有限的——把海量未经筛选的原始数据丢给它,关键信息会被噪声淹没,它反而更难抓住重点,且每轮都要重新处理这一大坨、又慢又贵;真正有用的,不是"把所有原始数据都给它",而是"把提炼过的、相关的、恰到好处的信息给它"——工具该返回的是"摘要/聚合/Top-N/分页",而不是"几万行原始记录"。根本原因是:工具返回未做大小控制,把原始全量数据直接塞进有限的上下文,既撑爆 token 上限,又用海量噪声淹没了模型的有效注意力;而我误以为"给模型的信息越多越好",没意识到上下文是稀缺资源、有效信息是提炼过的而非全量的。问题的根,是工具返回过大塞爆上下文——没做摘要/分页/截断,把全量原始数据灌给 LLM,既超 token 限制又淹没重点;根源是误以为信息越多越好,忽视了上下文有限、信息要提炼。这篇就把这次"工具返回塞爆上下文"的坑,从头到尾复盘一遍。
故障现场:几万行返回,撑爆了上下文
问题在于工具把原始全量数据直接塞进有限的上下文:
# 我给 Agent 的工具(没对返回大小做任何控制):
def query_database(sql: str) -> str:
rows = db.execute(sql).fetchall()
# ↓↓↓ 把所有行原封不动拼成字符串返回, 直接进上下文 ↓↓↓
return "\n".join(str(row) for row in rows) # ✗ 几万行 → 几十万token → 撑爆!
# Agent 生成了个没有 LIMIT 的查询:
# query_database("SELECT * FROM orders") # 几万行
# 工具返回几万行 → 拼进对话上下文 → 发给模型:
# openai.error.InvalidRequestError: This model's maximum context length is
# 128000 tokens, however your messages resulted in 350000 tokens. ← 撑爆, 报错!
# 我截断到"不超限"后, 问题依旧:
def query_database_v2(sql: str) -> str:
rows = db.execute(sql).fetchall()
return "\n".join(str(row) for row in rows)[:50000] # 粗暴截断到5万字符
# 现象: 不报错了, 但
# - 模型被几千行原始数据淹没, 抓不住用户真正关心的;
# - 每轮对话都带着这一大坨, token成本飙升、响应变慢;
# - 截断是"从中间切断", 可能切掉了关键行、留下半行脏数据;
# - 模型在噪声里"找重点", 回答质量明显下降。
"""
两个被我忽略的根本约束:
① 上下文窗口有限(有token上限): 它是LLM一次能"看到"的全部信息的容量, 是稀缺资源;
把几万行塞进去 = 直接超容量 = 报错; 或挤占了本该留给指令/历史/推理的空间。
② LLM的有效注意力有限: 即便塞得下, 海量未筛选的原始数据会淹没关键信号("大海捞针"变难),
模型抓不住重点、被噪声干扰, 回答质量反而下降——信息过载 ≈ 没有信息。
★ 我的错觉: "给模型的信息越多越全, 它分析得越好。"
真相: 上下文是稀缺资源, 有效信息是"提炼过的、相关的、恰到好处的", 不是"原始的、全部的";
工具该返回"摘要/聚合/Top-N/分页/结构化关键字段", 而非"全量原始数据"。
"""
看着那条 maximum context length exceeded 的报错,我又懊恼又恍然:"我一直觉得'资料给全一点,模型才好判断',谁知道几万行直接把它撑爆了;就算不爆,它也被淹得找不着北……原来不是给得越多越好。"这个坑最反直觉的地方在于:它违背了我们一个朴素的直觉——"信息越全越有利于决策";在人那里,我们会自然地略读、跳过、抓重点,感觉不到"信息太多"的代价;但对 LLM 来说,每一个 token 都占用有限的上下文、都消耗算力和成本、都可能成为干扰——"多给"不仅无益,还直接有害(撑爆、变贵、变笨)。下面就来拆解,工具返回到底该怎么设计。
第一件事:搞懂上下文是稀缺资源、信息要提炼
我顺着这次事故,把"给 Agent/LLM 喂信息"的原则彻底理清了。
为什么"工具返回全量数据"是错的? 该怎么给模型信息?
【核心: 上下文窗口是有限的稀缺资源、LLM有效注意力也有限; 工具返回要做大小控制(摘要/聚合/Top-N/
分页/结构化), 给"提炼过的相关信息"而非"全量原始数据"; 信息越多越好是错觉, 过载等于淹没】
1. 两个硬约束:
① 上下文窗口有限: 模型一次能处理的token有上限(几K到几百K); 工具返回、对话历史、系统指令、
模型的推理输出, 都挤在这个窗口里——它是稀缺资源, 寸土寸金;
② 有效注意力有限: 即便塞得下, 信息越多越杂, 模型越容易"迷失"在噪声里、抓不住关键(且越慢越贵)。
2. 工具返回过大的危害:
- 直接超限报错(对话崩);
- 挤占窗口, 把指令/历史/推理空间挤没了 → 模型"忘事"、跑偏;
- 淹没关键信息 → 回答抓不住重点、质量下降;
- 每轮都带着 → token成本和延迟持续飙升。
3. 工具返回的正确设计(把"全量"变"提炼"):
① 限制大小/分页: 默认加LIMIT、返回前N条、支持分页(offset/cursor)按需取下一页;
② 返回摘要/聚合: 别返原始几万行, 返"总数+统计+Top10+关键趋势"(让数据库做聚合, 不是把活儿丢给LLM);
③ 结构化提取关键字段: 只返回任务真正需要的列/字段, 砍掉无关的;
④ 大数据存外部, 返回引用: 把全量结果存文件/缓存, 工具返回一个ID/路径+摘要,
Agent需要细节时再用另一个工具按需读取特定部分(检索式, 而非一次性全塞);
⑤ 超限明确告知: 返回"结果过多(共N条, 仅显示前M条), 请缩小范围/分页", 让Agent知道并调整。
4. 更普适的原则: 给LLM的上下文要"精", 不要"全"
- 上下文工程(context engineering)的核心: 在有限窗口里, 放"最相关、最精炼、信噪比最高"的信息;
- 不是"能塞的都塞", 而是"只放完成当前任务真正需要的";
- 这和RAG(检索相关片段而非全文)、摘要、分页, 是同一个思想: 按需、提炼、相关。
5. 类比: 你给一个专家递材料
- 你不会把整个档案室搬给他, 而是递上"提炼好的、与问题相关的几页关键资料";
- 给他一卡车原始文件, 他反而被淹没、找不到重点——给LLM也一样。
一句话: 上下文窗口和模型注意力都是有限稀缺资源; 工具返回必须做大小控制(摘要/聚合/Top-N/分页/
结构化/外部存储+引用), 给"提炼过的相关信息"而非"全量原始数据"; "信息越多越好"是错觉。
这套认知,是整个坑的根。两个硬约束:①上下文窗口有限(工具返回/历史/指令/推理都挤在里面,稀缺);②有效注意力有限(信息越杂越抓不住关键)。工具返回过大的危害:超限报错、挤占窗口让模型忘事跑偏、淹没关键信息、成本和延迟飙升。正确设计:限制大小/分页、返回摘要/聚合(让数据库算)、结构化提取关键字段、大数据存外部返回引用按需再取、超限明确告知。普适原则:给 LLM 的上下文要"精"不要"全"——只放完成当前任务真正需要的最相关、最精炼的信息(同 RAG 思想)。一句话:上下文窗口和模型注意力都是有限稀缺资源;工具返回必须做大小控制(摘要/聚合/Top-N/分页/结构化/外部存储+引用),给"提炼过的相关信息"而非"全量原始数据";"信息越多越好"是错觉。
第二件事:正解——工具返回做摘要、分页、外部存储+引用
知道了上下文是稀缺资源,正解就清楚了:让工具返回"提炼后的、可控大小的"信息。
# 正解1: 限制大小 + 分页(默认不返回全量)
def query_database(sql: str, limit: int = 50, offset: int = 0) -> dict:
total = db.count(sql) # 先拿总数
rows = db.execute(f"{sql} LIMIT {limit} OFFSET {offset}").fetchall()
return {
"total": total, # 让Agent知道总共有多少
"returned": len(rows),
"offset": offset,
"rows": [dict(r) for r in rows], # 只返回这一页
"hint": f"共{total}条, 返回{offset}~{offset+len(rows)}; 需要更多请翻页或缩小条件",
}
# 正解2: 返回聚合/摘要, 而非原始全量(把计算下推给数据库, 别丢给LLM)
def order_summary(start: str, end: str) -> dict:
# 用户问"分析所有订单"→ 别返原始几万行, 返聚合统计
return {
"total_count": ..., # 总单数
"total_amount": ..., # 总金额
"by_status": {...}, # 各状态分布
"top_skus": [...], # 销量Top10
"daily_trend": [...], # 按天趋势
} # 几百token的摘要, 远比几万行原始数据有用
# 正解3: 大结果存外部, 返回引用 + 摘要, Agent按需再取(检索式)
def run_big_query(sql: str) -> dict:
rows = db.execute(sql).fetchall()
ref_id = cache.save(rows) # 全量存缓存/文件
return {
"result_id": ref_id, # 返回引用, 不返全量
"total": len(rows),
"preview": [dict(r) for r in rows[:5]], # 给个预览
"columns": list(rows[0].keys()) if rows else [],
"hint": "结果较大已暂存; 用 read_result(result_id, range) 按需读取具体部分",
}
def read_result(result_id: str, start: int, end: int) -> list:
return cache.load(result_id)[start:end] # Agent需要哪段再取哪段
# 正解4: 工具描述里就约束Agent(配合529篇: 工具描述要清晰)
# 在 query_database 的描述里写明: "默认返回前50条; 大范围查询请用 order_summary 获取聚合;
# 不要查询无 LIMIT 的全表" → 引导模型生成合理的调用。
# 核心: 工具返回要"提炼+可控": 默认分页/限量、能聚合就返聚合、大数据存外部返引用、明确告知总量;
# 让进入上下文的, 是"完成任务所需的精炼信息", 而非"原始全量"。
这套正解的关键,是让工具成为"信息的提炼器"而非"原始数据的搬运工"。限制大小 + 分页:默认返回前 N 条、给出总数和翻页提示,别一次全返。返回聚合/摘要:用户要"分析"时,让数据库做聚合统计(总数/分布/Top-N/趋势),返几百 token 的摘要,远胜几万行原始数据。大结果存外部 + 引用:全量存缓存/文件、只返回引用 ID + 预览,Agent 需要细节时再用另一个工具按需读取(检索式,而非一次全塞)。工具描述里约束:在描述中写明默认限量、大范围用聚合工具,引导模型生成合理调用。核心是:让进入上下文的,是"完成任务所需的精炼信息",而非"原始全量"。
第三件事:其他几个"上下文管理"相关的坑
顺着这次塞爆上下文,我把 Agent 上下文管理相关的几个坑也一并理了:
几个Agent上下文管理的坑(核心都是"上下文是稀缺资源, 要精心管理"):
坑1: 对话历史无限累积(同517篇)——多轮后历史越堆越长, 撑爆窗口/成本飙升;
正解: 滚动摘要(把旧历史压缩成摘要)、只保留最近N轮+关键信息、按需截断。
坑2: 把整个文件/文档塞进去——读个大文件全塞, 同样撑爆;
正解: 用RAG只检索相关片段; 或分块处理; 按需读取特定部分。
坑3: 多个工具返回累加——一轮调好几个工具, 每个都返回不少, 累加起来也爆;
正解: 每个工具都限量; 必要时对中间结果做摘要再继续。
坑4: 把原始数据丢给LLM做计算/统计——让模型"数几万行里有多少条" 既不准又烧token;
正解: 确定性的计算/聚合/排序交给代码/数据库(它们准且便宜), LLM负责理解和决策(同585: LLM不是计算器)。
坑5: 不告诉Agent"数据被截断了"——悄悄截断, Agent以为看到了全部, 据此下错结论;
正解: 明确返回"共N条仅显示M条", 让Agent知道并决定是否翻页/缩小范围。
坑6: 把敏感/无关字段也塞进去——既浪费上下文, 又有泄露风险;
正解: 只返回任务必需的字段; 脱敏。
共同的根: 上下文窗口是LLM最宝贵也最有限的资源; 一切进入它的信息都应"经过筛选、提炼、按需";
"把能拿到的都丢给模型"是偷懒且有害的——真正的功夫在于"只给它完成任务所需的、最精炼的那部分"。
这些坑看似不同,根却是同一个:上下文窗口是 LLM 最宝贵也最有限的资源;一切进入它的信息(工具返回、历史、文档),都应当经过筛选、提炼、按需供给,而不是"有啥塞啥、能塞多少塞多少"。认清这个根("上下文稀缺、信息要提炼按需"),才能设计出不被自己的数据淹没的 Agent。
第四件事:全量塞 vs 提炼返回——两张对照表
我把"把全量原始数据塞进上下文"和"返回提炼信息"逐项对照,整理成表,贴在了团队的 Agent 开发规范里:
| 维度 | 全量原始数据(错) | 提炼后返回(对) |
|---|---|---|
| 返回内容 | 几万行原始记录 | 摘要/聚合/Top-N/分页 |
| token 占用 | 巨大,常超限 | 可控,几百到几千 |
| 是否超上下文 | 极易超限报错 | 不超限 |
| 模型抓重点 | 被噪声淹没,抓不住 | 信噪比高,易抓重点 |
| 成本/延迟 | 每轮都高且累加 | 低且稳定 |
| 计算谁做 | 丢给 LLM 数/算(不准) | 数据库/代码算(准且便宜) |
| 细节获取 | 一次性全给 | 按需分页/引用再取 |
| 需求 | 该返回什么 |
|---|---|
| "分析/总结这些数据" | 聚合统计(总数/分布/趋势/Top-N) |
| "列出符合条件的项" | 分页 + 总数 + 翻页提示 |
| "找出某个具体的" | 精确查询,只返回那一条/几条 |
| "后续可能要看明细" | 引用 ID + 预览,按需读取 |
| "算个数/求和/排序" | 代码/SQL 算好,只返回结果 |
这两张表的核心,第一张是"提炼后返回"在每个维度都优于"全量塞"——不超限、抓得住重点、便宜、计算更准;第二张是按 Agent 的真实需求(分析/列举/查找/明细/计算)决定返回什么,几乎都不需要"全量原始数据"。记住一条:问自己"Agent 完成这个任务,真正需要的是这几万行本身,还是从中提炼的结论?"——答案几乎总是后者。
第五件事:关于给模型喂信息的几组容易想当然的认知
这次事故也让我厘清了几组关于"给 LLM/Agent 信息"的、容易想当然的概念:
| 直觉以为 | 实际上 |
|---|---|
| 给模型的信息越多越全越好 | 上下文有限,过载会超限并淹没重点 |
| 把数据都给它,它自己会挑重点 | 海量噪声里它更难抓重点,质量下降 |
| 工具返回原始数据最"忠实" | 忠实但有害,该返回提炼后的相关信息 |
| 让模型统计/计算原始数据很自然 | 不准且烧 token,该交给代码/数据库 |
| 截断到不超限就行了 | 粗暴截断丢关键、留脏数据,该结构化提炼 |
| 上下文窗口大了就不用管返回大小 | 窗口再大也有限且贵,信噪比仍要管 |
| 信息传递就是把东西原样送过去 | 好的传递是按接收方需要提炼后送 |
这张表里,我栽的是第一行和第二行:抱着"信息越多越全越好、模型自己会挑重点"的直觉,把工具的全量原始返回直接灌进去,既撑爆了窗口,又淹没了模型。厘清这些,核心是一个意识:上下文窗口是 LLM 稀缺而宝贵的资源;给它信息的艺术,是"提炼与按需",而非"全量与堆砌"——你的工具、你的上下文管理,要做"信息的提炼器和守门人",只让最相关、最精炼的信息进入。
第六件事:设计 Agent 工具返回时,我现在的自检习惯
现在每当我设计一个 Agent 工具的返回,我都会先按这张图问自己:
这张图的精髓,是"返回要提炼可控、计算交给代码、明确告知总量、按需供给"。先问会不会很大、再看Agent 真正要什么(分析就聚合、量大就分页、要细节就引用)、计算别丢给 LLM、告知总量和截断。这套习惯,让我从"查到啥都塞给模型"变成了"只给它完成任务所需的精炼信息"——核心始终是:上下文窗口和模型注意力都是有限稀缺资源;工具返回必须做大小控制(摘要/聚合/Top-N/分页/外部存储+引用),给提炼过的相关信息而非全量原始数据,信息越多越好是错觉。
我立下的几条规矩
这场"工具返回塞爆上下文"的事故,换来了我做 AI Agent 时,刻进骨子里的几条铁律:
- 上下文窗口是有限的稀缺资源,工具返回、历史、指令、推理都挤在里面,寸土寸金。
- 工具返回必须做大小控制:默认分页/限量,绝不一次返回全量原始数据。
- 能聚合就返聚合(总数/分布/Top-N/趋势),让数据库/代码算,别把原始数据丢给 LLM。
- 大结果存外部、返回引用 ID + 预览,Agent 需要细节时按需读取特定部分。
- 明确告知 Agent 总量与是否截断("共 N 条仅显示 M 条"),别让它据残缺数据下结论。
- "信息越多越好"是错觉:过载会超限、淹没重点、变慢变贵,有效信息是提炼过的。
- 给 LLM 的上下文要"精"不要"全":只放完成当前任务真正需要的最相关、最精炼的信息。
附:一个通用的"大返回收口"装饰器
借这次的坑,我给团队写了个装饰器,给所有工具的返回统一套上"大小收口",防止某个工具忘了做限制就把上下文撑爆。
# 通用装饰器: 给工具返回兜底, 超过阈值就转为"摘要+引用", 防止塞爆上下文
import json, functools
MAX_CHARS = 4000 # 单个工具返回进上下文的字符上限(按token预算定)
def cap_tool_output(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
result = fn(*args, **kwargs)
text = result if isinstance(result, str) else json.dumps(result, ensure_ascii=False)
if len(text) <= MAX_CHARS:
return result
# 超限: 存外部, 只返回预览+引用+明确提示, 而非粗暴截断
ref_id = cache.save(text)
return {
"truncated": True,
"total_chars": len(text),
"preview": text[:MAX_CHARS],
"result_id": ref_id,
"hint": f"返回过大(共{len(text)}字符), 仅显示预览; "
f"用 read_result('{ref_id}', start, end) 按需读取, 或缩小查询范围",
}
return wrapper
@cap_tool_output # 任何工具加上它, 就不会因为忘做限制而撑爆上下文
def query_database(sql: str) -> str:
...
# 原则: 在框架层给"工具返回进上下文"设一道统一的兜底闸门, 把"别撑爆上下文"从
# "每个工具作者都要记得" 变成 "框架自动保证"——纵深防御, 别指望每处都不出错。
这个装饰器是一道"兜底闸门":即便某个工具的作者忘了做分页/摘要,框架层也会拦住超大返回、转成预览+引用,不让它撑爆上下文。它的理念和前面 586 篇的幂等封装一脉相承:对于"一旦疏漏就会出事"的横切约束(上下文大小、幂等、限流),与其依赖每个开发者都记得,不如在框架层设一道统一的、自动生效的防线——纵深防御,把"正确"变成"默认且兜底的"。
写在最后
回头看,这场由"工具返回全量数据塞爆上下文"引发的、Agent 当场超限又抓不住重点的事故,真正教给我的,远不止"工具返回要分页和摘要"这一个技巧。它让我对"'把更多的信息提供出去' 和 '让接收方更好地理解、决策', 并不是正相关的; 当信息量超过接收方的'有效处理能力'时, 更多的信息反而会淹没关键、降低质量——'多' 会变成 '噪声'",有了一次刻骨的体会。我栽跟头,是因为我朴素地以为"信息越全,判断越准"——我以为把几万行数据毫无保留地交给模型, 就是对它最大的帮助;可我忽略了:任何接收者(模型、人、系统)的'处理带宽和注意力'都是有限的; 当我倾倒的信息远超它能有效消化的量时, 我给的就不再是'养分', 而是'洪水'——它会冲垮容量(超限)、淹没关键(抓不住重点);真正的帮助, 不是'给得多', 而是'给得对、给得精'——替接收方做好筛选和提炼, 把信噪比最高的那部分递过去。这让我领悟到一个关于"信息、带宽与提炼"的深刻认知:有效的信息传递,核心不是"传递的信息量",而是"在接收方有限的处理能力内, 传递了多少'有效、相关、可消化'的信息"——"信噪比" 远比 "信息总量" 重要; 超出处理能力的信息过载, 约等于没有信息, 甚至不如没有(因为它还淹没了有用的);这对一切"向有限带宽的对象传递信息"的场景都成立——给模型喂上下文、给领导写汇报、给同事讲方案、做 UI 给用户呈现信息: 把'原始的全部'一股脑倒出去是偷懒, 替对方'提炼出相关的精华'才是真正的功夫和体贴。这给了我一种传递信息时的自觉:无论是给 AI、给人、还是给系统传递信息,都要先想"接收方的有效处理能力有多大?它完成当前这件事, 真正需要的是哪部分信息?"——然后替它做好筛选、提炼、聚合, 只递上信噪比最高的、按需的那部分, 而不是把我手上的原始信息原封不动地倾倒过去;"以接收方的处理能力为约束、以相关性和信噪比为准绳去提炼信息",是从'信息的搬运工'升级为'信息的提炼者'的关键。认清信息越多不等于越有用、超出处理能力的过载会淹没关键、有效传递靠信噪比而非总量——这,是我用一次工具返回塞爆上下文的事故,换来的、关于 AI Agent、也关于如何向任何对象有效传递信息的、最朴素也最深刻的领悟。如果这篇复盘,能让你下次给 Agent 写工具、或给任何人传递信息时,先停下来想一句"对方真正需要的是哪一小部分",而非"我能给的全部",那我对着那条 context length exceeded 的报错复盘的这段时间,就值了。
—— 别看了 · 2026