最近我在做一个基于 Spring AI 的微信智能助手。项目早期的思路很直接:接入大模型,把天气、健康档案、冰箱库存、菜谱、营养计算、运动计划、提醒等能力包装成 @Tool,再交给模型自己选择。
刚开始,这种开发体验确实很顺。写一个 Service,外面包一层 Tool,注册到 Spring AI,模型马上就多了一项能力。我当时很自然地认为:Tool 越多,Agent 就越强。
但随着工具增长到十几个、业务流程开始跨越多个领域,问题逐渐出现:响应变慢,输入变长,工具调用顺序不稳定,一些只读查询会被重复执行,排查问题时也很难判断到底是模型决策、上下文还是业务代码出了偏差。
这轮重构之后,我最大的认识是:
Agent 工程真正难的地方,不是让模型拥有更多能力,而是判断什么时候根本不该让模型参与。
从自由 Tool Loop 开始
项目早期的主链路很典型:
用户消息→ ChatClient→ 模型读取 Prompt、历史与全部 Tool Schema→ 模型选择 Tool→ Tool 执行并返回结果→ 模型继续决策或组织最终回复对于“今天杭州天气怎么样”这样的单步问题,这套流程没有明显问题。模型选中天气工具,取得数据,再组织一句自然语言回答。
但“今天三个人,预算 80 元,帮我安排一顿晚饭”完全不同。它背后可能需要读取健康档案和库存,生成菜谱,计算食材需求与营养,生成采购清单,最后校验预算。如果把这些步骤全部交给模型自由编排,执行过程就会变成反复的 LLM → Tool → LLM。
从演示效果看,这很有 Agent 的味道;从工程角度看,它却把一个已知流程重新变成了概率问题。调用顺序可能变化,某一步可能遗漏,同一个只读工具可能重复执行,模型也可能在拿到中间结果后重新规划整条链路。
一旦流程已经足够确定,让模型继续负责流程编排,通常不会增加价值,只会增加延迟、Token 消耗和不确定性。
Tool 不是免费的能力列表
过去谈 Token 优化,我首先想到的是聊天历史。但一次 Agent 请求里,模型看到的不只是用户的当前消息,还可能包括 System Prompt、Tool Definitions、参数 JSON Schema、Memory、RAG 结果、历史消息和上一轮 Tool Result。
Java 里一个看似简单的方法:
@Tool(description = "查询用户冰箱库存")public PantryResult getPantry(...) { ...}到了模型侧,会展开成工具名称、描述、参数类型、字段说明和必填约束。三个工具时,这部分成本很小;当工具越来越多时,即使用户只问“今天几号”,请求中也可能携带菜谱、库存、营养、运动和提醒等完全无关的 Schema。
工具数量因此同时影响四件事:输入 Token、Prefill 时间、候选选择难度,以及误调用机会。给 Agent 注册所有能力对开发者很方便,却不一定是合理的运行时设计。
优化顺序应该从“能否不调用模型”开始
一开始我也想过继续压缩 System Prompt。但把一段 Prompt 从 1000 Token 缩短到 800 Token,收益往往不如直接消除一次没有必要的模型请求。
后来我的优化顺序逐渐变成:
1. 这一步能不能不用模型?2. 能不能减少一次模型调用?3. 能不能缩小 Tool 候选集合?4. 能不能只加载当前任务相关的上下文?5. Tool Result 能不能只保留必要字段?6. RAG 和 Memory 能不能按相关性限量?7. 最后再压缩 Prompt 文案。这个顺序改变了问题的性质。Token 优化不再只是 Prompt Engineering,而变成了模型调度和系统边界设计。
把确定流程移出 Agent Tool Loop
项目里后来引入了 BusinessSkill。它的作用很简单:一个业务如果已有明确步骤,就由 Java 代码稳定编排,不再让模型每次现场决定下一步。
以晚餐规划为例,原先可能由模型自由选择健康档案、库存、菜谱、营养和采购工具;重构后,DinnerBudgetPlanningSkill 直接调用对应的领域服务,确定执行顺序、参数校验、预算判断和状态变化。
模型只保留它真正擅长的部分:
- 理解用户想完成什么;
- 从自然语言中抽取参数;
- 判断表达是否存在歧义;
- 把结构化结果解释得自然、清楚。
而业务系统负责调用顺序、数据读写、事务、幂等、权限与确认。对这些已经确定的逻辑来说,模型的随机性不是能力,而是额外风险。
不是每一句话都值得问模型
BusinessSkill 解决了执行阶段的确定性,路由阶段还可以继续前移。项目现在的处理思路可以概括为:
Control Command→ ExpectedInput→ RuleBased Router→ Intent Model Router→ 其他兜底路径原则是:越确定的问题,越靠前用本地代码处理。
控制指令:零次模型调用
如果系统正等待用户确认运动记录,那么“确认记录运动”“取消”“继续”这类回复的含义已经由会话状态限定,没有必要再让模型解释一次。本地状态机可以直接处理。
ExpectedInput:系统应该记得自己刚问过什么
假设机器人上一轮问“预算是多少”,会话里已经记录 expectedField = budget 和字段类型。用户回复“100”“一百块”或“预算一百左右”时,本地解析器可以直接填充参数。
如果系统明明知道自己刚问了什么,却还把用户回答连同一大段历史发给模型重新推理,这是一种很隐蔽的浪费。
规则路由:高置信表达直接命中
“查看运动记录”“查看健康档案”“今天几号”这类边界清晰的表达,可以由规则直接路由。只有“最近想稍微练一练,但又不想太累”这种包含目标、程度和隐含约束的表达,才值得进入意图模型。
我现在更愿意把 LLM 看成歧义处理器,而不是所有请求的默认入口。
Context 不等于最近 N 条聊天记录
项目早期会把最近一段聊天历史直接交给模型。实现简单,但微信只有一个聊天窗口:用户可能先问天气,再规划运动,接着聊晚餐,之后又转到内容创作。“最近 20 条消息”与“当前任务需要的信息”很快就不再是同一件事。
无关历史不只消耗 Token,还会污染当前任务。模型可能把上一次任务的预算、人数或偏好误带到这一次。
因此,我现在把上下文理解为:
当前目标+ 当前任务状态+ 已确认约束+ 当前缺失参数+ 最近真正相关的消息聊天历史只是 Context 的一种原材料,而不是 Context 本身。
用 SkillSession 保存状态,而不是让模型反复回忆
以晚餐规划为例,与其让模型每轮从历史里重新寻找人数、预算、忌口和执行进度,不如把这些信息直接保存为结构化会话状态:
skill: DinnerBudgetPlanningSkillaction: PLAN_DINNER
collectedParams: people: 3 budget: 80
missingFields: - dietaryPreference
expectedInput: dietaryPreference下一轮用户只说“不吃香菜”,系统已经知道当前等待的是 dietaryPreference。这既减少历史重放,也避免模型重新推理流程位置。
业务状态应该存在业务系统里,而不是只存在 Prompt 里。
真实聊天一定会被打断
用户不会按开发者设计好的顺序聊天。机器人刚问“几个人”,用户可能突然插一句“对了,今天杭州会下雨吗”。
项目把这类关系区分为当前话题、只读打断、新话题、混合请求和歧义请求。天气查询属于典型的只读打断:系统可以保存当前晚餐 Skill,回答天气,然后继续等待原先的人数输入。
这里真正帮助上下文管理的,不是更大的上下文窗口,而是 ConversationRuntimeContext。它明确记录当前活跃任务、暂停任务、待确认操作、话题版本和恢复位置。
因此,“不要无限保存 Prompt,要保存状态”不只是 Token 技巧,也是多轮交互可靠性的基础。
长期上下文要压缩成可使用的结构
短期任务可以保留少量相关消息,长期使用则不能无限累积。更合适的方式,是把详细历史逐步压缩为结构化 Summary:
Goal: FAT_LOSS
Constraints: people: 3 weeklyBudget: 300 dislikedFood: - 香菜
Confirmed: healthProfile: true
OpenItems: - 下周菜单待确认和一段模糊的自然语言回忆相比,结构化摘要更短、更稳定,也更容易被程序裁剪和复用。Context Engineering 真正研究的,不是怎样塞入更多内容,而是这一轮哪些信息值得进入模型。
Tool Result 也要面向模型裁剪
数据库实体里可能包含 ID、用户标识、创建时间、更新时间、版本号、内部状态和审计字段,但模型规划晚餐时,真正需要看到的也许只是“鸡胸肉 500g、鸡蛋 6 个、番茄 4 个”。
如果把完整 Entity 直接序列化回模型,所有内部字段都会变成 Token,也可能暴露模型不需要接触的实现细节。更合理的边界是:
Domain Entity→ 面向模型的精简 DTO→ LLM系统内部保留完整业务数据,只把完成当前推理所需的字段交给模型。数据存在,不代表模型需要知道。
还没有完成的两项优化
目前项目仍保留了一部分通用 Tool Calling,这里还有两个明确的后续方向。
第一是动态 ToolBundle。普通聊天可以不暴露工具,天气问题只暴露天气与日期,菜谱问题只暴露库存、菜谱和营养;真实写操作则直接进入对应 BusinessSkill,不交给通用 Agent 自由选择。
第二是按用途拆分 Prompt Profile,例如意图路由、普通聊天、工具 Agent、规划、内容生成和输出修复。意图路由只需要知道有哪些 Skill、Action 以及结构化输出约束,不需要同时背负内容创作和最终回复风格。
这两项目前是下一阶段设计方向,而不是已经验证完成的能力。
这次重构留下的三个结论
一、Tool 越多,不等于 Agent 越强
工具数量增加会扩大 Schema、候选空间和误调用机会。真正重要的是,正确的工具能否在正确的时候被暴露。
二、Context 不等于 Chat History
上下文更接近目标、状态、约束、决策、待补参数和相关事实的组合。历史消息只是其中一种来源。
三、最省 Token 的方法,是少让模型处理确定性
金额解析、单位换算、状态机、权限、数据库事务、公式、幂等判断,这些都更适合由代码稳定完成。LLM 的价值应该留给语言理解、歧义判断、信息抽取、开放生成与解释。
写在最后
做这个项目之前,我更关注“怎样让模型学会调用所有工具”。现在我更常问的是:“这一轮为什么要让模型看到这个工具?”
模型擅长处理自然语言中的不确定性,工程系统擅长状态、规则、事务、类型和确定性。两者真正配合起来以后,Agent 才不只是一个会调用 API 的聊天机器人,而开始像一个能够长期维护的业务系统。
确定性的事情尽量交给代码,不确定性的事情才值得交给模型。
如果这篇文章对你有帮助,欢迎分享给更多人!
部分信息可能已经过时






