我给查数 AI 的「话痨」装了个音量旋钮:50 行太少 200 行太多?那就让用户自己拧
这两天 AI 圈还在卷 Agent 的"聪明程度":更大的上下文窗口、更长的记忆、一次能吞下整库。但有个反直觉的现实没人爱提——查数 Agent 最大的敌人不是"看不见",而是"看太多"。一次查询吐回 5000 行,上下文当场撑爆,token 烧得哗哗的,模型还被淹没在数字里找不着北。
我没跟着卷窗口长度。我干了另一件事——去翻了一个开源数据智能体工作台(daw,「寒鸦数据工作台」)的代码,看它怎么给查询工具"限流"。它的解法跟模型半毛钱关系没有:一个行数上限,从硬编码 50,进化成用户手里的旋钮。
看完我可以负责任地告诉你三件事。
第一,截断多少行不是技术问题,是产品判断——daw 的作者在 2026 年 9 月 7 日那天"拍板":50 改成 200,理由只有一句"明细下钻常需超 50 行"。
第二,一个数字在代码里出现四次,改漏一次 AI 就瞎猜——所以它先被提成常量,再被测试钉死,最后升级成设置项。
第三,最漂亮的设计是"把度交给用户,把界留给自己":用户可以在 10~1000 之间随便拧,但手改配置文件想拧到 5000?门都没有,代码会把它钳回来。
1. 先看病:为什么查数工具必须"截断"
先给大家科普一个概念「上下文是按行烧钱的」:Agent 查一次数,数据库返回的每一行都会变成 token,塞进模型的上下文窗口。一张明细表动不动几千行,全塞进去的后果有三个——窗口撑爆直接报错、token 费用原地起飞、模型在数字海洋里注意力涣散,该看的聚合数反而看不见。
翻译成人话:查数工具不截断,等于让 AI 用吸管喝水库——要么呛死,要么淹死。
daw 的工具描述里把纪律写得很直白:结果超过 N 行自动截断;全量先拿聚合函数算总数,明细再按需下钻。注意这个顺序——聚合先行,下钻按需。这不是截断,这是查数的方法论。
但这里有个魔鬼细节:工具描述里写着"超过 50 行自动截断",代码里真的是 50 吗?这就是下面要讲的故事。
2. 第一步:先拍板一个数——50→200
故事的起点是一个"用户拍板"。2026 年 9 月 7 日,daw 把查询行数上限从 50 改成 200,commit 信息里写的原因朴素到只有一句话:明细下钻常需超 50 行。
翻译成人话:真实用下来发现,50 行根本不够看明细——用户问"上个月退货最多的 10 个 SKU 是哪些",聚合算完,总得下钻看看明细长什么样吧?50 行卡在那儿,刚看到点意思就没了。所以拍板改成 200。
这一步是地基:截断数不是算出来的,是用出来的。没有"理论最优行数",只有"在真实会话里够不够用"。先定一个"当前最合理"的数让它跑起来——先有判断,再有机制;反过来先做配置界面的,默认值往往是个拍脑袋的数字。
学完这一步你能带走的第一个方法:给 Agent 工具定输出上限时,先从真实使用场景里找一个"够用"的数,写死它,跑两周再说。过早的可配置化,配置的是你的不确定,不是用户的需求。
3. 第二步:一个数字,四处引用——提成常量
50 改 200 的时候,daw 的作者发现一个要命的问题:这个"50"在代码里出现了四次——
- 实际落库的查询包装(
wrap_query(&sql, Some(50)),SQL 层面加 LIMIT) - 执行层的查询调用(
run_query(&guard, &sql, Some(50))) - 工具描述(告诉模型的:“结果超过 50 行自动截断”)
- 截断文案(告诉用户的:“(结果已截断,仅返回前 50 行)”)
翻译成人话:第 1、2 处是"实际发生的事",第 3 处是"告诉 AI 的事",第 4 处是"告诉用户的事"。改漏第 3 处,代码截 200 行,模型却以为只截 50 行——它会基于错误假设瞎推理。对 Agent 来说,描述与实现不一致就是"说明书和机器对不上":不报错,只犯浑。
所以第二个 commit 干了一件很"笨"的事:定义const ROW_LIMIT: usize = 200;,四处统一引用它。注释里写得斩钉截铁:"包装查询、执行、工具描述与截断文案四处统一引用本常量,防止漂移。"同时系统提示(preamble)里原来写死的数字也同步掉。
学完这一步你能带走的第二个方法:凡是会同时出现在"给模型的描述"和"给机器的代码"里的数字,必须是同一个常量。这是 Agent 开发独有的纪律——传统软件里描述和实现漂移只是文档 bug,在 Agent 系统里,描述就是模型世界观的一部分,漂移了模型就活在虚假的世界里。
4. 第三步:写个测试,把描述和常量钉死
常量统一了,但人性是靠不住的——下次有人改了常量忘了改描述,或者改了描述的措辞把数字写错了,漂移又回来了。daw 的解法是:写一个测试,专门盯着这件事。
#[test]fnrow_limit_matches_tool_description(){forlimitin[DEFAULT_ROW_LIMIT,10,1000]{assert!(tool_description(limit).contains(&format!("超过 {limit} 行自动截断")),...);}}翻译成人话:拿默认 200、下限 10、上限 1000 三个值分别渲染描述,断言里面的数字和传入值对得上。以后谁改描述模板漏了数字,测试直接变红。
这个测试的名字就叫"防漂移"。我认为是全篇最值得抄的一行代码:它把"文档与实现一致"从一种期望,变成了一道门禁。很多团队的 Agent 工具描述都是手写文案,改代码时顺手改不改全看良心——daw 选择不赌良心,赌测试。
学完这一步你能带走的第三个方法:给你的每个 Agent 工具写一个"描述一致性测试"——描述里承诺的每一个数字、每一个行为,都要有测试断言它和实现对得上。改实现不改描述,应该和改坏逻辑一样被测试拦下来。
5. 第四步:从硬编码到"用户自己拧"——设置项 queryRowLimit
常量+测试跑稳之后,daw 才做了最后一步:把行数上限升级成用户可配的设置项queryRowLimit。注意这个时机——不是一上来就做配置,而是在"默认值经过验证、一致性有测试兜底"之后才开放。顺序反了,配置项就会变成"我也不知道多少合适你自己试"的甩锅。
这个设置项的设计有五个细节,每个都值得抄:
第一,缺省 200。不配置就是 200——那个经过真实会话验证的数。配置项的默认值不是"随便填",是"大多数人不用改也好用"。
第二,钳位 10~1000。代码注释写得直白:"行数直接进 LLM 上下文,过大的单查即可撑爆窗口(前端 UI 限范围,这里防手改文件)。"翻译成人话:设置页输入框能限范围,但用户可以直接改settings.json——手改个 100000 进去,下次查询就把上下文撑爆了。所以读取时钳位:小于 10 按 10,大于 1000 按 1000。把"度"交给用户,把"界"留给自己。
第三,读取容错,一律回落默认。文件缺失、JSON 损坏、键缺失、值不是数字——一律回落 200,不报错不崩溃。翻译成人话:用户把配置文件改坏了,程序默默用默认值继续工作,而不是崩给用户看。
第四,改完下一查即生效,无需重启。实现很"糙快猛":每次执行查询重读一次设置文件。tiny JSON 读一次开销忽略不计,换来"改完下一查即生效"——没有缓存,就没有缓存失效问题。
第五,工具描述动态注入当前值。工具描述不再写死"超过 200 行自动截断",而是按当前设置值动态渲染——用户改成 500,模型看到的就是"超过 500 行自动截断"。描述必须说真话:实现会变,描述就得跟着动。
截断文案也升级了:“(结果已截断,仅返回前 N 行。可在设置中调整行数上限)”——限制用户之前,先告诉用户门在哪。
6. 为什么这样设计:藏在四步背后的三条纪律
回头看这四步——拍板数、常量统一、防漂移测试、可配化——其实是三条纪律的展开:
纪律一:描述即世界观。在 Agent 系统里,工具描述不是文档,是模型理解世界的窗口。描述里的每一个数字都必须为真,且必须持续为真。这就是为什么 daw 愿意为"描述里的数字"专门写一个测试——它保护的不是文档质量,是模型的世界观不崩塌。
纪律二:先判断,后机制。50→200 是判断(从真实场景来),常量统一是工程,可配化是产品。很多团队的顺序是反的:先做个配置项,默认值拍脑袋,描述手写不管一致性。daw 的顺序是:数字先在真实会话里被验证,再用工程手段保证不漂移,最后才交到用户手里——交出去时上下界和容错都已想好。
纪律三:双通道输出。还有个藏在代码里的细节:返回给 LLM 的是"紧凑文本"(列名+前 N 行纯文本,避免灌满上下文),而"完整结构化结果"通过 payload 发给前端 UI。翻译成人话:模型和人看到的不是同一份结果——模型拿够推理用的就行,前端展示用人要看时有全量数据。一份数据两种包装,各取所需:截的不是数据,是进模型的那条通道。
给开发者的建议
- 先定一个"用出来的数",再谈可配置。从真实会话里找截断点,写死跑两周。过早的配置项配置的是你的不确定。
- 描述与实现共享同一个常量。凡是同时出现在工具描述和代码里的数字,必须是同一个常量的两次引用。这是 Agent 开发的专属纪律。
- 给工具描述写一致性测试。描述里承诺的数字和行为,用测试钉死。改实现不改描述,应该被测试拦下来,和改坏逻辑同等对待。
- 开放配置时同时给出"界":缺省、钳位、容错三件套。缺省是"不用改也好用",钳位防手改文件作死,容错保证配置文件坏了程序不崩。
- 配置变更要热生效,描述要动态注入。读一次 tiny JSON 的开销,换来"改完即生效";描述里的数字必须跟着当前值变——描述说真话,模型才不瞎猜。
- 截断时告诉用户门在哪。“(结果已截断,仅返回前 N 行。可在设置中调整行数上限)”——限制之前先给出口,这是产品的基本礼貌。
说到底,daw 的行数上限回答了一个 Agent 开发者都要回答的问题:模型一次该"看"多少?答案是:先看够用的,再保证看到的是真的,最后把遥控器交到用户手里——但遥控器两头永远有钳子守着。不替用户做决定,但替用户守住底线。