有一个迷思在 Agent 圈子里极其流行,且流毒甚广:能力越多越好,文档越全越稳。给模型多塞一份文档,看起来是给它多装备一件利器;给 Skill 多写一条规则,看起来是给系统多上一道双保险。我原来对此也深信不疑,直到亲眼看着一次原本寻常的执行,把输入 token 从区区 3 千一路一路干到恐怖的 38 万。
那次执行的输入序列至今让我记忆犹新:3,106 → 30,526 → 55,105 → 380,178。凶手很快就被排查到了:两次看似无害的 Jira 全文搜索,text ~ "split tips" 以及 text ~ "tip allocation",入参设了 maxResults: 50。返回的结果极其热心地带着 description、comment、attachment、issuelinks 全家桶。合计约 1.03 MB 的巨型载荷,一股脑儿地直接塞进了 Agent 的 scratchpad 里。而工具结果这东西一旦进了 scratchpad,后面每一轮模型调用,都必须原封不动地把这段历史再交一遍门票——果不其然,第八次调用时,直接把上下文给撑爆了。最委屈的是 Postgres chat memory,被我们当成第一嫌疑人查了半天,结果盘查下来它只有区区 3 KB,纯属路过围观的无辜群众。
更阴险的是这个故障的表达方式:Agent 节点实际在底层已经撞上了 context window 崩溃错误,但流程里的 onError: continueErrorOutput 逻辑强行把它导进了错误分支,导致 workflow 整体在监控看板上依然高高兴兴地标记着 success。你以为你在监控系统整体的成功率,实际上你只是在监控最后一个节点收工时的心情。

先搞清楚 token 是怎么翻十倍的
事故发生后,很多人的第一反应极其典型:「上下文爆了?那直接去调大 LLM 的窗口,或者对 memory 做智能压缩啊。」这也是我们最初怀疑 memory 的主要原因——直到深入日志发现它只有 3 KB。真正的死因比想象中更朴素,也更致命:Agent 的工具结果在设计上缺乏生命周期管理。一次搜索返回了 1 MB 的数据,它绝对不是「模型看了一眼就算了」,而是从此永久搬进 scratchpad 扎根,后续的每一轮模型推理调用,都必须把这 1 MB 原样重新读取一遍。第一轮 1 MB 是单次开销,到了第八轮它就变成了定时炸弹——每一次调用都在为同同一份陈旧的结果重复支付高昂成本,直到模型彻底拒载。
这揭示了极其关键的两件事。
第一,工具输出的体积绝非什么无关紧要的实现细节,它本身就是接口契约的核心组成部分:一个可能吐出 1 MB 数据包的搜索接口,在工程上等价于在每次 Agent 调用时抽签决定要不要把上下文直接炸掉。
第二,「让 Agent 自己去摘要」根本不是可行的解法——摘要这个动作发生在模型读取完全文之后,而读全文这个动作本身就已经越界了。炸弹在拆弹专家赶到现场之前,就已经在内存里引爆了。
当时团队内部也认真研讨过「增大 Chat Memory」这条路,但很快就被干脆利落地否掉了:memory 全程只有 3 KB,给它扩容无非是给旁观者加座,治标不治本。「降低 maxIterations」的方案倒是保留了下来,但也仅仅被当成防守底线的兜底策略——它能限制最坏情况下的轮次上限,却根本阻止不了单次超大结果的瞬间物理毁灭。这就像在马路上设立限高杆:它确实能拦住轰轰烈烈的车队,却拦不住一辆自己本身就是栋高楼的怪物卡车。
最终的重构修复方案显得顺理成章:把原本混为一谈的搜索和取证,彻底拆分成两个独立阶段。
在候选筛选阶段,只允许使用极为收窄的 JQL,强制限制返回 5~10 条,且只许带轻量字段——像 summary/status/issuetype 这种基础户口本信息。至于 attachment 和 issuelinks 这种笨重家具,一律不许在这个阶段搬进上下文。只有当候选精准命中后,再按具体的 issue ID 逐条去钻取深层的 description 和 comment。外加在入口端强制用项目名称、issue type 或父任务把 JQL 的检索范围严格收窄,最大程度减少客服工单那种全家桶式的噪声干预。
38 万 token 这种惨剧,在这一套新结构约束下,从物理机制上就不可能再发生——你不是在苦口婆心地教模型省吃俭用,而是直接把暴饮暴食的物理通道从建筑结构层面拆除了。

默认认知预算是一等指标
这次血淋淋的生产事故,彻底让我想明白了一个在 Agent 架构中极为核心的概念:Skill 的默认认知预算。也就是说,每次触发这个 Skill,模型都必须被迫读完并严格遵守的规则成本。这笔昂贵的认知成本,在每一次、每一个用户请求发起时都在被强制收取,跟你这次请求到底用不用得上这些规则完全无关。
回顾历史,我们的 Jira Skill 恰恰就是这样一步步吹气球般发胖的。最开始的时候,只保留 read/search/search-user/update/link 这几个清晰的原子操作,整体显得非常干练。但后来,团队陆陆续续塞进去了「创建 Story 必须强制走 Release Note」的整套复杂业务流程:包括字段收集、预览确认、创建动作、撰写 Release Note、建立关联关系以及失败后的补偿机制。
每次往里塞规则的时候,产品和工程的理由都显得无比正当且无可挑剔——「为了防止有新人跳过预览步骤直接写入」「为了防止有人忘记建立关联」。每一条规则都像给仓库门上多加的一把锁,加到最后,连开门找一把钥匙都需要整整五分钟。于是乎,后续每次哪怕有人只是想极其简单地查个 issue 状态,模型都不得不先在脑海里通读半本厚厚的《企业发布管理条例》。
在后来的设计评审会上,有人拉出了直观的数据:系统里的提示词总词数在涨、NEVER 负向清清单在涨、决策树的分支节点在涨——这三个「涨」字合在一起,直接导致了模型对规则的漏遵循概率大幅上涨。更糟糕的是,不同规则之间开始出现极其微妙的互搏现象:一条规则严厉告诫「创建前必须进行完整预览」,另一条规则却在强调「短查询千万别绕弯路」。模型无所适从地站在岔路口开始思考人生,最终的结果就是哪条规则都没能完美执行。
这就是「Skill 越写越多,Agent 却越来越笨」的微观运行机制——根本不是你的大模型突然变笨了,而是你每天都在往它的办公桌上疯狂堆放大量毫无关联的杂物文件。
至于怎么拆,这里面有着极其讲究的工程哲学:必须坚持按意图频率和事务边界来拆,绝对不能简单地按命令数量去拆。
- 通用 Skill(保留高频原子路径):高频发生的只读或简单原子路径,继续保留在通用的主 Skill 中。但在这里只加一条极其干净的硬性路由规则:「一旦检测到 create Story 意图,立刻无条件转交业务 workflow」。
- Workflow Skill(收拢低频长事务):低频但跨越多个步骤、有着严格执行顺序和不可跳过约束的复杂事务,整体完整搬进专属的 workflow skill 中。在里面集中管控字段校验、预览、创建、Release Note 生成、关联建立和异常补偿。
这里需要格外强调的是「整体」这两个字。Release Note 绝对不能单独被拆成一个孤立的 Skill,否则它一旦脱离了创建的主流程上下文,后半段原本设想的强约束反而更容易被模型直接绕过。「业务相关」这四个字,不足以成为多条规则强行共处一室的理由:前半段的字段校验与后半段的 Release Note 共享的是同一个事务生命周期,强行把它们拆散,等同于把结婚证的一半直接发给了邻居。
拆分完成后,系统在 runtime 层的运行方式也必须想得透彻,绝不是把代码文件挪个地方就算大功告成。
入口处的 Skill 首先进行轻量级的意图分类:如果是只读或者单次原子更新,直接高效执行;一旦精准命中跨步骤的长事务,立刻把已经解析出来的最小上下文打包转交给对应的 workflow skill。Workflow 会在一个显式的 run 运行时里,精准记录阶段状态、幂等键、用户确认标识以及外部对象 ID。一旦中途发生失败,能够从最近已提交的阶段继续重试或者执行补偿——而不是寄希望于主 Agent 凭着脆弱的记忆去盲目续跑。
主 Skill 也不再盲目复制 workflow 内部复杂的字段规则,而仅仅保留三样核心要素:清晰的触发条件、严禁绕过的项、以及严格的结果契约。
当然,在这套架构里依然埋着几个当时尚未完全解答的难题:跨 Skill 转交时的上下文状态到底要不要做严密的 schema 和版本控制?纯自然语言的转交会不会在边缘场景下丢失必填字段?用户如果在预览阶段临时修改了字段,幂等键和 Release Note 草稿该如何保持实时同步?这些都是架构拆分后必然带来的隐藏税,我们需要逐笔记账,但绝不装作看不见。

同一招治了三种病
事实证明,这套围绕「默认认知预算」建立起来的底层解题思路,在后来的演进中被我们大规模复用,顺手治好了三个看似形态各异、但实质病根高度同源的生产疑难杂症。
1. 按需 Skill 的延迟加载与渠道契约
好不容易省下来的认知预算,差点直接拿系统的核心可靠性去做了无谓的交换。最终,我们只能把「普通 output 对 Slack 用户天然不可见」这一底层事实明确写入通信协议中,并强制要求 SLACK-MESSAGE-SKILL 在当次执行生命周期内的第一次发送动作前,必须完整加载一次,用以统一管控 mrkdwn、链接转换、mention 提醒、字符转义以及 thread 的路由逻辑。
2. 两阶段检索的标准化工程落地
- 第一,问一句「这个工具可能返回的最重的一条结果到底有多大?」
- 第二,问一句「调用方在 90% 的场景下,真的需要事无巨细地拿到每一条结果的全部细节吗?」
3. 工具发现分层与长尾归因迷局
重构后的修复方案是在处理工具命名、描述注入以及 sandbox bridge 隔离之前,先在底层进行彻底的递归展开操作:不管是普通的 Tool、包含 getTools() 方法的对象、.tools 属性,还是深层嵌套的数组,统一全部平铺剥离成最底层的叶子工具,然后再谈具体的过滤与挂载逻辑。
在发包和构建环节,我们也付出了相当沉重的代价,踩过极其低级的坑:曾经有一版 2.1.1 的 build 在 CI 流水线上显示全绿通过,但打包出来的 tarball 压缩包里却奇葩地漏掉了 dist 编译目录,导致发布到 NPM 上的是一个彻头彻尾的空壳包。从此之后,发布的门禁流水线里被强制加了目标 Node 版本的实际 load 校验以及 tarball 内容的严格检查——「published 成功」再也不等同于「在生产环境能被正常加载」。
MCP 和 Skill 不是等价包装
其中最核心的缺口在于跨统一字段的批量编辑能力——本地 Skill 只能傻傻地通过循环发起单条的 edit 请求,既没有 dry-run 预演校验机制,也没有批量执行后的状态回查能力;其次是在混合批量 upsert、模块维度的整体移动与重命名、以及敏感删除操作前的破坏性影响预检与审批流联动。
评审得出的最终结论绝非非黑即白的「二选一」,而是精准定位各自的架构分工:
- MCP 的定位:应当牢牢锚定在固定的 QA 项目中,作为 Agent 的原生通用接口。它的强项在于具备极其严密的批量处理契约和深度的领域不变式校验——例如叶子模块校验、
node_id/node_path的一致性约束、以及写后自动回查验证,这些都是它的拿手好戏。 - Skill 的定位:应当作为跨项目迁移的通用轻量工具箱,它的核心强项在于极高的可移植性。
两边在底层应当共享一套 canonical 字段规范 Schema、dry-run 预演语义以及统一的写后校验逻辑,而不是各自为政地并行维护两套严重重叠的 CRUD 代码,然后眼睁睁地看着它们在后续的演进中各自发生逻辑漂移。「两个方案都能实现增删改查」绝对不等于「它们在工程上是等价的」——工具链的合并与收拢,核心要比较的是批量处理语义、领域不变式约束、审批流集成以及写后回查机制,而不是简单地去比对谁的 API 命令清单更长。
怎么证明拆对了
拆分重构工作完成之后,绝不能仅仅停留在自我感觉良好的主观层面,系统到底有没有被拆对,至少需要通过以下三类标准的 Eval 自动化评测集来进行硬核验证:
- 防误触评测:验证日常高频发生的纯 read/search 查询请求,绝对不会因为语义模糊而误触发昂贵且危险的创建流程。
- 强约束评测:验证发起 create Story 的指令时,流程 100% 无法绕过 Release Note 的撰写环节。
- 幂等与补偿评测:验证当 Release Note 生成步骤遭遇网络异常抛错失败时,系统的补偿机制绝不会在后台悄悄重复创建出第二个冗余的 Story。
- 每次任务调用的上下文 Token 消耗量
- 意图路由的准确率
- 复杂任务的最终端到端完成率
- 运行过程中需要人工介入纠正的次数
总结成一句话:Skill 的架构设计从来不是什么技术人员的强迫症整理癖,它本质上是一门严谨的系统经济学。 那些每次调用都强制要求模型阅读的硬编码规则,就是系统里流动着的硬通货。在肆意花掉它们之前,务必先冷静地问自己一句——这条规则,真的值得让后续十万次完全无关的请求,跟着一起买单吗?