有一类 Agent 问题,排查起来特别费劲。 它不报错、不超时,日志里全是成功;模型也返回了一段结构完整、语言通顺的答案。但答案依赖的事实基础少了一大截——数据源有 50 条记录,真正进入模型上下文的只有 20 条,剩下 30 条没有报错、没有空值、也没有显眼异常,模型只是基于自己看到的那 20 条继续回答。 掘金作者 hpoenixf 在《MCP返回了 50 条数据,模型只看到了 20 条:一次 Agent 静默截断排查》里把这个过程完整记了下来。它把一句很别扭的话摆到了台面上:Agent 不一定是"看全了以后答错",也可能是系统从一开始就没有把完整数据交给它。 一、最初,它看起来只是"回答质量不稳定" 同样一批资料,有时候总结得不错,有时候明显漏掉后面的内容。第一反应很自然:是不是上下文太长?是不是模型注意力不够?要不要在 prompt 里强调"不要遗漏任何条目"? 往前查才发现这些方向都不对,因为模型根本没有机会看到那些数据。 链路是:权威数据源 → 读取 / 过滤 / 配额限制 → 准备模型输入 → LLM 总结 → 最终回答。问题发生在 LLM 之前——源端 50 条,中间层有一个条目上限,只取了 20 条。对模型来说,这 20 条就是它的全部世界,它既不知道后面还有 30 条,也没有理由主动说"我可能没看全"。 二、先让系统能说出一句话:这次输入不是完整集合 作者没有继续调 prompt,而是把问题往前移了一层,先回答几个朴素的问题:源端这次有多少条?调用方希望处理多少条?真正进入模型输入的是多少条?有没有因为字节限制被截断? 这些信息以前散落在实现细节里,后来被提成了显式字段,简化后大致是:requested 为 50、loaded 为 20、byteComplete 为 true、filteredCount 为 0、inputComplete 为 false、reason 为 entry_limit。 重点不是字段叫什么名字,而是系统终于能明确说出:这一次送给模型的输入不是完整集合。这句话应该由代码判断,而不是让模型自己猜。如果 inputComplete 为 false,后面的回答至少不能继续假装自己基于全集。 三、数量都对上了,也只证明了一件事 假设看到 requested = 50、loaded = 50、filtered = 0、byteComplete = true,能不能说"数据完整,回答可信"?不能。它最多证明这 50 条目标记录都进入了模型输入,没有被已知的数量、字节或过滤规则截掉,但没有证明模型真的充分利用了它们。 所以作者把"完整"拆成三层:源集合是否定义完整 → 目标数据是否完整进入模型 → 最终回答是否正确覆盖这些数据。第一层里,"全集"是有定义的:比如用户当前 37 个持仓,比如某个 API 分页任务应该读取 8 页。 而且单纯数数量也可能得到假的完整。源端目标是 A B C D E,中间层实际拿到 A B C D D:数量仍然是 5,但 E 已经丢了,只是 D 重复了一次。所以对有稳定身份的有限集合,最好再加一层 identity 对账,reason 写成 identity_mismatch。实现不一定非要 hash,item ID 集合、page cursor、snapshot version 都可以,关键只有一句:数量一致是必要信号,不是充分证据。 四、静默丢数据其实有三种 第一种最直观:条目数被截断。源端 50 条,配置只允许 20 条,剩下 30 条在进入模型之前就消失了。如果系统不记录原始数量,后面基本没人知道发生过什么。 第二种更隐蔽:字节数被截断。条目数量没超限,但某几条特别长,累计输入超过 byte budget 后尾部被截掉。这时你甚至可能看到 requested = 20、loaded = 20——数量完全正常,但第 20 条其实只剩一半。 第三种是解析和过滤阶段偷偷丢记录。某条数据格式异常、某字段为空、schema 不兼容,转换函数抛错后被 catch 掉,系统选择 skip 然后继续处理剩余内容,最终依然能产生漂亮的模型输入。这也是为什么"读取成功"这种过于粗的状态不值得信任。 五、配额不能一路调大,测试要故意构造失败 最直接的修复是把 20 改成 60,在当前案例里这确实解决了问题。但如果总结成"上下文越大越好",很快会踩另一个坑:输入越大,延迟越高、token 成本越高、模型注意力更分散。配额真正要解决的不是"尽可能塞进去",而是"超出单次可靠处理范围时,系统应该知道自己没处理完"。作者总结得很干脆:配额本身不是问题,静默配额才是问题。 这件事最后真正稳定下来,不是因为代码看起来合理,而是因为有人故意构造了一个会失败的输入:上限是 60,就准备 65 条记录,并且把真正关键的数据放在最后几条。测试不再只检查"函数返回了结果",而是检查 requested = 65、loaded = 60、inputComplete = false、reason = entry_limit;如果是字节超限,则应该得到 byteComplete = false、reason = byte_truncated。一直只用 5 条、10 条数据测,系统可能几年都不会真正走一次截断路径,然后第一次走,就是线上。 作者也交代了这次改动证明了什么、没证明什么:受控窗口里确实复现了 50 条只进 20 条的静默截断,65 条尾部丢失用例能稳定触发不完整分支,截断专项从初次 6 失败、2 通过变成当次 8 项全部通过。但这没有说明模型从此不会漏读。"工程复盘最容易犯的错误之一,就是修了一个确定的问题,顺手把旁边五个没验证的问题也一起宣布解决。" 六、MCP 在这一层帮不上忙 有人会想:MCP 不是统一了工具接入吗,协议层能不能兜住? 按掘金作者杨杨杨大侠在《MCP 到底接在了哪一层?从"Agent 调工具"说起》里的拆解:模型负责提出要做什么,应用负责把它做成一次真实调用,MCP 约定的是应用与外部服务之间怎么对接。模型只给出了调用意图,真正去取数据、处理权限、拿回结果的,是运行这个助手的应用。 也就是说,协议规定了"这些能力怎么被请求",没有规定"你拿到了多少、丢了没有"。数据在应用内部被配额截断、被字节截断、被过滤器 skip 掉,这一层既不在模型的视野里,也不在协议的管辖里。 七、该被约束的不是语气,是断言的边界 早期做法比较保守:只要输入不完整,回答就只能是草稿。方向没错,但写得太死也有问题——部分数据并不意味着所有结论都失效。 比如 100 个持仓里只拿到了 80 个。你当然不能说"这就是你的完整持仓分布",但如果问题只是"这 80 个已读取持仓里有没有黄金 ETF",在明确范围之后仍然可以回答:在当前已读取的 80 个持仓里没有发现黄金 ETF,另有 20 个未读取,不能据此判断完整组合没有黄金敞口。 这里真正应该被限制的是断言范围,而不是文风。部分输入只能对已覆盖范围作答,同时披露缺失范围,禁止把局部结论升级成全集结论。 写在最后 以前我们更关注"模型答得对不对"。现在做 Agent,值得往前多问一句:它回答之前,到底看到了什么?一段语言完整、逻辑顺畅的回答,可能只是对一个残缺输入做出的合理总结——这时候模型甚至没有做错什么,真正的问题发生在更前面:系统丢了数据,却没有告诉任何人。 原则很简单:对有明确全集的数据任务,先证明输入覆盖,再讨论回答质量。但输入覆盖完整,只证明模型有机会看到全部,不证明它真的理解了全部。 本文事实与数据来自以下原文: · 掘金《MCP返回了 50 条数据,模型只看到了 20 条:一次 Agent 静默截断排查》 https://juejin.cn/post/7688929298951880719 · 掘金《MCP 到底接在了哪一层?从"Agent 调工具"说起》 https://juejin.cn/post/7688528701484253225 · MCP 架构规范 https://modelcontextprotocol.io/specification/2026-07-28/architecture
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。