ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

AI Agent外壳设计:如何用提示词、工具协议和编排决定智能体上下限

AI Agent外壳设计:如何用提示词、工具协议和编排决定智能体上下限

如果你最近在看 AI Agent 相关的实践项目,多半会撞上一个很有意思的现象:同一个底层模型,有人拿它做出来的 Agent 能稳定跑完复杂的多步骤任务,有人做出来的却总在简单环节上翻车。模型没换,训练权重没动,差异全出在套在模型外面那层壳上。这篇就把这个"同一个模型,换个外壳差很多"的现象拆开讲清楚,聊聊外壳到底在解决什么问题、它如何同时决定 Agent 的下限和上限,以及我们在实际搭建外壳时应该把功夫花在哪。

“外壳”这个词听起来轻巧,实际包含提示词结构、工具协议、上下文管理、编排逻辑、状态维护和人工确认机制。这一整套东西不动声色地决定了:模型是认真完成你的目标,还是在上下文里迷路;是稳定输出可靠结果,还是偶尔给你一步错步步错;是扛得住用户的真实流量,还是稍微一并发就报“模型繁忙”。这篇内容适合正在搭 Agent 的应用开发者、做技术选型的负责人、以及研究大模型落地的产品经理,不讲虚的,直接聊逻辑和实操。

1. 为什么模型没变,干活的样子却完全不一样

1.1 大模型不是执行器,而是"语义补全器"

要理解外壳的价值,先要接受一个底层事实:大模型不是一台严格按指令执行的计算机,它本质上是一个庞大的语义补全器。给它一段前文,它做的事情就是预测下一个最合理的 token,然后再预测下下个。所谓"思考",其实是它在你的 prompt 框架内做续写。

这就带来一个关键推论:模型输出的质量,极大程度上取决于"你给它看了什么样的前文"。你给的信息结构越清晰、目标越可分辨、约束越明确,它就越容易沿着正确的语义路径补全;你给的信息混乱、任务目标含糊、工具描述模糊,它就会按它脑子里"最可能的下一句话"去发挥,而这往往不是你想要的。

这也解释了一个日常现象:同一个模型 API,直接让它在对话框里完成复杂任务,和把它放进一个设计良好的 Agent 外壳里完成同样的任务,成功率完全不同。你的 prompt 结构、工具定义、上下文组织方式,就是那个"前文"的物理载体。外壳不是在帮模型思考,而是在帮模型"正确地进入补全状态"。

1.2 同样一个模型,两种接法的差距有多大

我来举一个很常见的业务场景:客服 Agent 要完成"查订单、判断是否可退、执行退款"这个三步操作。同一个 GPT-4 级别接口,用两种不同壳去接,结果是天差地别的。

第一种做法很朴素:把所有工具的函数描述堆在 system prompt 里,然后把用户的历史对话一股脑塞进上下文,告诉模型"你可以调用工具来解决用户问题"。实际跑下来你会看到:模型有时选错工具,把查订单的工具误认为退款工具;有时不懂得先查后退,直接虚构一个退款结果;有时因为上下文太长,把订单号给漏了,甚至从早期对话里翻出一个完全不相干的数字当订单号。

第二种做法,在外壳上加了三样东西:意图路由、结构化上下文摘要、严格 JSON Schema 工具协议。用户消息进来后,外壳先判断当前意图是"查询"还是"售后",再连同用户身份、近期订单摘要、当前会话状态整理成一个紧凑的结构化上下文,最后把所有工具的入参约束用 schema 锁死,模型只能按格式输出工具调用。同样一个模型,在这个外壳里几乎不再乱选工具,退款流程的成功率直线上升。

差距不在模型,而在信息是怎么被组织到模型面前的。这组对比很直观地回答了"为什么换个外壳差很多"——模型的下限不是模型自己兜底的,是外壳用结构、约束和流程帮它兜底的。

1.3 上限和下限分别指什么

一个容易被忽略的点在于,外壳同时影响 Agent 的上限和下限,但影响的方式不同。

所谓下限,是 Agent 在最常规、最简单任务上的稳定性。它不要求惊艳,但要求可依赖:一百次调用里不能出现几次乱来、漏步骤、输出坏 JSON、对话一长就状态错乱。这些稳定性指标几乎全由外壳决定,模型反而问题不大,因为简单任务模型本身就能做,关键是外壳有没有把路铺平、把出错路径堵住。

所谓上限,是 Agent 在复杂长任务上的可能性边界。它能不能把十几个子步骤串联起来不丢中间状态;能不能在决策点上综合多个信息源做判断;能不能在遇到分支情况时走到合理的兜底路径。这些能力有一部分来自模型的知识和推理底盘,但更大的部分来自编排架构。如果外壳没有状态管理、没有记忆分层、没有流程分支,模型再强也发挥不出来。

所以对一个工程团队来说,模型选型解决的是底盘质感问题,而外壳设计解决的既是你这辆车不会轻易散架的问题,也是你能不能跑远程复杂路况的问题。两者都重要,但从投入产出比看,多数团队的瓶颈在后者。

2. 外壳的下半身:工具协议和上下文管理决定稳定下限

2.1 工具描述不是注释,而是模型的操作手册

在实际工程里,工具描述写得不好是 Agent 翻车的头号原因。很多初版外壳里,工具函数只写了名字和参数类型,比如get_order(order_id: str),然后就没有了。模型看到这种描述,其实不知道这个工具是干嘛用的、什么时候该用它、用了会返回什么东西。

大模型理解工具的方式完全依赖文字描述。工具怎么命名、怎么描述功能边界、参数要写多细、什么时候明确告诉模型"这个场景不适合调用",都会直接影响模型的选择准确率。我自己踩过的坑是:某个工具叫"查询商品信息",描述里没有说明它不包含库存状态。结果模型在处理"还有货吗"这类问题时频繁错误调用它,返回的商品页里根本没有库存字段,后面的判断全部建立在幻觉上。

后来我调整了工具描述写法,每条描述故意带上三个信息:功能边界(做什么)、触发场景(什么时候用)、反面约束(什么时候别用)。比如"查询商品信息,用于获取商品标题、价格、规格;当用户询问库存数量或发货状态时请使用 inventory 工具,不要使用本工具"。改完之后,工具选择的准确率几乎翻倍。

这里有一个通用原则:工具描述是写给模型看的操作手册,不是写给别人看的代码注释。每条工具描述都应该采用"人话+边界+触发条件+反例"的结构,必要时给出调用示例。别怕描述长,模型处理文字的带宽远比处理混乱语义的成本低。

2.2 上下文一长就翻车:剪枝、摘要与记忆分层

另一个决定下限的问题是上下文管理。很多 Agent 第一版都是把整段对话历史原封不动塞进模型,反正接口支持长上下文。结果系统 prompt 加工具定义再加七八轮历史之后,关键信息稀释在几千 token 里,模型开始把用户的旧问题当成当前任务,或者被中间一段无关讨论带偏。本质问题不是模型"记不住",而是外壳没有帮它筛选"该记什么"。

好的外壳会对上下文做分层。第一层是固定信息,包括身份设定、任务目标、工具定义;第二层是会话状态,包括当前意图、正在处理的订单号、已完成的步骤;第三层才是最近的原始对话,用于理解即时话锋。在进入模型之前,外壳会把第二层状态直接从业务数据里取出来,拼成一个紧凑快照,而不会让模型从历史对话里自己翻。这一步很像人处理工作:你不会把一年前的邮件全部重读一遍再做今天的决策,你只看今天这份简报。

上下文较长时,可以用摘要压缩而非直接截断。比如每轮对话结束后,把前几轮总结成一句带关键字段的摘要,保留在顶层。对长会话,我通常会设定一个窗口,超过某个轮数就把更老的原始对话折叠成结构化摘要。这个"折叠"动作还要特别注意数值型字段,比如订单号、金额、日期——这些信息绝不能丢,否则到了退款环节就是灾难。

2.3 模型输出不听话时:校验、重试、人工兜底三层保险

不管 prompt 写得再好,模型总有概率输出格式错误的内容,比如 JSON 里套了 Markdown、顺手多打了一个字段、把refund写成了refuns。如果外壳不设防,一次格式错误可能让整个流程直接崩掉。处理这个问题要有三层兜底:第一层是 schema 校验,第二层是自动纠正加重试,第三层是人工确认兜底。

第一层出现的问题最多。我遇到的极其常见的情况是:模型多轮对话中间偶尔输出了一段"思考过程"作为 JSON 值,schema 解析直接失败。简单重试效果有限,因为模型在同样的上下文里大概率会重复同样的坏习惯。真正有效的做法是捕获校验失败的具体错误,把它回传给模型,并附上修正提示。比如告诉它"你刚才输出的字段status枚举值不合法,可接受值为 processing/succeeded/failed,请重新输出 JSON"。这相当于给模型一个明确的纠错信号。

第三层兜底其实很实用:对于高影响操作(退款、删除、外发消息)不要允许模型直接执行,外壳把模型生成的参数留待人确认后才能真正调用。这是把 Agent 的信任边界从"模型可信"转移到"流程可信"的关键一步,也是把下限兜住最稳妥的做法。

3. 外壳的上半身:编排架构和并发承载决定能力上限

3.1 编排架构的演进路线:从直连到图式工作流

外壳的另一个大头是编排。模型在单轮对话中能力再强,也不可能在一个 call 里完成非常复杂的多步任务。这时候外壳就要负责把任务拆解成多轮模型调用,并控制它们之间的衔接。不同的编排方式直接决定 Agent 能处理的任务复杂度的上限。

最基础的形态是单轮直调,用户问一句,模型答一句,最多带一次工具。这种形态适合翻译、摘要、分类这类单步任务,优点是简单可控,缺点是完全不具备处理多步骤事务的能力。

高一阶是 ReAct 循环:模型思考下一步该做什么,调用工具,看到结果后继续思考下一轮,循环直到任务完成。这是很多 LangChain 初版应用的样子。它能处理多步任务,但有两个隐患——一是容易陷入无意义循环,反复调用同一个工具拿一样的结果;二是中间状态全存在对话历史里,环境一复杂就混乱。

高阶形态是图式工作流,用 LangGraph 这类工具把任务显式建模成节点和有向边。比如把客服流程建成一个图:意图识别 -> 订单查询 -> 售后判断 -> 退款执行,每个节点是明确的模型调用或工具调用,节点之间用类型化的状态对象传递数据,而不是靠把历史对话塞给模型去猜当前在哪一步。这种架构把"流程控制"和"模型语义能力"解耦了,模型只负责节点内的决策和生成,流程转移由编码逻辑决定。实测下来,图式工作流在处理长流程任务时的稳定性和可维护性,明显高于 ReAct 这类自由循环。

再往上还有多 Agent 协作架构,不同角色 Agent 分别处理不同子领域,通过消息相互协作。这种模式适用于问题面很宽的场景,但代价是语义损耗和调试复杂度同步上升,小团队慎用。

3.2 状态管理是编排的命门:丢状态等于失忆

任何编排架构,只要涉及多轮调用,就一定绕不开状态管理。很多 Agent 项目做大了以后开始频繁出现"上一步结果下一步就忘了"的问题,多数不是模型健忘,而是外壳没做好状态传递。

在 ReAct 架构里,状态往往隐式存在对话历史里,靠模型自己从上下文中感知当前进度。这在短链路里没问题,但链路一长,模型的注意力会漂,就会"忘记"自己已经完成了哪些步骤。这本质上是因为状态不是结构化的,而是散落在语义里。

正确的做法是把状态显式建模。外壳里维护一个 state 对象,包含当前节点、已完成步骤、已获取的关键数据、待处理分支列表。每次进入节点前,外壳把 state 中的关键字段注入到上下文中;节点执行完毕后,把新产出回写到 state。这样模型永远只负责当前这一步,不用从几千 token 历史里去倒推"我现在到底进行到哪了"。状态集中在代码里,还能顺便解决可观测性问题——任何时候想知道 Agent 在干嘛,查一下 state 就行。

如果你要设计一个通用一点的参考,可以沿用这个思路,在代码里就是循环里挂状态机:

state = { "current_node": "intent_classify", "order_id": None, "intent": None, "steps_done": [], "needs_human_confirm": False, } while state["current_node"] != "finished": node = workflow[state["current_node"]] state = node.run(state, llm)

3.3 并发问题:“模型繁忙”时外壳该怎么扛

再往上走,参与一次真实的 Agent 项目,遇到的一定是并发问题。热搜里有人问"AI Agent 怎么扛并发",说到底是重建一套共识:大多数 Agent 应用线上大面积超时,瓶颈往往不在模型服务本身,而在外壳层完全没有做并发相关的治理措施。

模型服务第一次“升级”时会收到 "模型繁忙" 的错误码,实际上就是在告诉你“上游打满了”。如果外壳不做处理,只是把错误直接抛给用户,这个 Agent 在业务上就是脆弱的。我自己做线上 Agent 应用时总结了三个手段:缓存、排队、降级。

缓存是最容易被忽略的一个。同一个用户的同一个问题,在短周期内被反复问到,其实是很常见的现象。外壳层可以对"输入问题 + 用户身份"做一个短时缓存,命中就直接返回结果,不消耗模型调用。一次缓存命中,可能在高峰期帮你省下 30% 的模型压力。

排队针对的是模型服务限流的场景。外壳层可以对模型调用封装一层请求队列,并用令牌桶限制对上游的调用速率,避免一瞬间把所有任务都打到模型上。队列缓冲之后配合合理的轮询状态,可以有效减轻“模型繁忙”触发。

降级是针对模型能力的策略。高峰期把复杂推理降级为通用回答,或者降级到更便宜的模型。注意,降级的时候一定要把任务上下文压缩得更紧再发给小模型,因为小模型的指令遵循能力弱,更需要显式约束。这个技巧结合后面要说的“本地模型”话题一起看,特别有感触。

4. 从零搭一个可用的外壳:选型与最小实践

4.1 技术栈怎么选:框架不是越重越好

聊完原理,落到选型。现在市面上的 Agent 框架五花八门,我见过用 Spring AI Agent 的、用 LangChain 的、用 LangGraph 的、用 Rust 自研的,甚至还有直接用 FastAPI 手搓循环的。每个方案都有它的位置,但选型之前必须先想清楚一个问题:你的业务是"高频短任务"还是"低频长流程"?

如果是高频短任务,比如客服问答、内容分类、信息抽取,这类任务的核心诉求是低延迟、高并发、易维护。结论往往是:别上太重的工作流框架,直接把模型调用做一层薄封装,配上缓存、限流和工具协议,反而比套大框架更稳。我曾见过团队为了一个单轮信息抽取应用引进了整套多 Agent 编排框架,结果主要体现在调度复杂、改一个 prompt 要翻三层抽象,线上问题不好定位。

如果是低频长流程,例如工单处理、项目方案生成、竞品调研报告,这类任务的核心诉求是状态持久化、任务分解、分支处理。LangGraph、Temporal 这类带显式状态和工作流语义的方案更合适。框架能帮你处理好任务恢复、中间状态保存、人工审批节点这些看似不起眼但实际决定成败的机制。

下面是一个简化版对照表,仅供参考:

方案适合场景维护成本可控性
FastAPI + 手写循环高频短任务、团队熟悉 Python低高
LangChain快速原型验证中(版本变动大)中
LangGraph复杂工作流、显式状态中高中高
Spring AI AgentJava 技术栈、企业集成中中高
Rust 自研极高压并发、资源敏感高极高

4.2 最小可用外壳的骨架代码

不管用哪个框架,一个最小的可用外壳基本都包含这几块:模型接口、工具注册表、状态对象、编排循环、人工确认机制。用一个很朴素的 Python 伪代码来呈现这套骨架最好理解,它不依赖任何框架,方便你迁到 LangGraph 或者其它平台上:

class AgentRuntime: def __init__(self, llm): self.llm = llm self.tools = {} # 工具注册表 self.state = {"steps": []} # 显式状态 def register_tool(self, name, description, schema, handler): self.tools[name] = { "description": description, "schema": schema, "handler": handler, } def run(self, task): # 1. 组织上下文:把状态摘要 + 工具手册 + 任务描述拼起来 context = self._build_context(task) # 2. 调用模型,要求输出严格 JSON 工具调用 response = self.llm.chat(context, response_format="json") action, args = self._parse_action(response) # 3. 执行工具 + 校验 + 回写状态 if action in self.tools: result = self.tools[action]["handler"](**args) self.state["steps"].append(action) return result def _build_context(self, task): tool_manual = self._tool_manual() state_snapshot = json.dumps(self.state) return f""" 当前进度: {state_snapshot} 你的任务: {task} 可用工具: {tool_manual} 只输出 JSON 工具调用。"""

骨架之所以强调“工具注册表 + 状态快照 + 严格输出格式”,就是因为这三样就足以把前面聊到的几类问题全部兜住:模型不会在模糊的工具描述里乱选,状态不会丢在历史里,输出格式有校验基础。把这个跑通之后,再在循环外面加缓存、限流、人工确认节点,就是一个能上生产的壳了。

4.3 给外壳装"刹车":护栏设计的几个细节

外壳里最容易被忽略的是护栏,但护栏恰恰是最体现工程经验的部分。做 Agent 不能只考虑"让它干活",还要考虑"它不能干什么"。我常用的几个设计:

第一,不要把系统 prompt 里的限制当成安全边界。真正需要禁止模型操作的动作,比如删除数据、修改核心配置、外向发送消息,应该是"模型根本看不到这些工具",而不是"模型被提示不要调用"。我们需要通过工具可见性来控制能力边界,而依赖模型的自觉不靠谱。

第二,高影响操作一定要加入人工确认节点。有些人觉得加人工确认影响体验,但真实业务里"机器确认 + 人确认 + 审计日志"是保命符。一次漏掉审批,可能比一百次体验折损更致命。

第三,给所有工具调用加上审计日志。每次模型调用了什么工具、传了什么参数、拿到什么结果、经过了哪一层校验,全部落日志。Agent 的项目调试非常依赖完整链路回放,日志有时比功能本身还要重要。日志记录做好了,你才能知道模型在哪一步开始偏离、是哪一段上下文误导了它。

5. 什么时候别硬套外壳:减法思维是真功夫

5.1 简单任务用重框架,反而会把模型变蠢

讲了很多外壳设计之后,我想说一个反方向的观点:外壳不是越复杂越好。给简单任务套上复杂的编排架构,效果往往适得其反。比如单轮问答任务,硬上了多 Agent 协作、多轮反思机制,结果是模型每句话都要被"反思"一遍,把原本干净的回答改得冗长别扭,还把本来应该保留的话改了意思。复杂度本身就是成本,会带来更多的故障点。

判断过度设计有一个简单标准:任务能不能用一个模型调用完成?如果能,就别上多轮编排;用一次调用加一次工具调用能完成,就别上多 Agent。外壳的设计目标是"恰好够用",不是"看起来高级"。

5.2 模型实力不够时,外壳能补多少

还有一个避免不了的问题:模型本身能力不够,外壳能帮忙到什么程度?我经常看到有人说"只要提示词写得好,小模型也能干大活",这话对一半。外壳确实能把小模型的下限大幅度抬起来——通过严格 schema、显式状态、窄化任务边界、给足示例,小模型可以在特定窄场景下干得有模有样。但模型的知识储备和推理底盘是壳补不了的。

比如用强外壳 + 小模型去处理长文档归纳和新知识联想,小模型会频繁出现事实偏差和逻辑跳跃,外壳只能让它在规定格式里犯错,不能让它不犯错。本地模型和云端模型的差距,在复杂语义场景下就是存在。外壳的意义在于让模型能力得到充分释放,而不是凭空创造能力。

所以对模型能力有限的场景,务实的做法是:把任务切得更细、范围收得更窄、由外壳承担更多工程逻辑,而不是把希望全押在模型自己会"理解"上。这也是为什么同一个模型的本地版接外壳后能解决很多实际问题,但离"通用多面手"还很远。

5.3 我自己的几条实操心得

最后分享几条干活积攒下来的体会,不成体系,但都很实用。

第一条,永远先跑通最小链路再上框架。任何 Agent 项目我都建议先用一个脚本把"输入-模型调用-工具执行-输出"跑通,确认业务逻辑本身成立,再换到正式框架里去补状态管理和并发治理。直接上框架的结果往往是你搞不清楚是业务设计的问题,还是框架配置的问题。

第二条,每次调外壳只盯两个指标:任务成功率(或完成率)、人工干预率。理想情况下,外壳改动应该让成功率上升、干预率下降。如果改了 prompt 结构,发现任务完成率没变而 token 消耗涨了,说明这个改动没有意义。用指标说话,而不是凭感觉评价外壳好坏。

第三条,对外壳做改动时,一次只改一个变量。模型的语义系统高度非线性,你又是在外围调整,同时换工具描述、改状态结构、调 prompt,失败了根本不知道是哪一步影响的。一次改动跑一批测试样例,对比看效果,再动下一处。

第四条,可观测性永远要优先于功能开发。Agent 和普通程序最大的不同是:它的“逻辑仓”存在于黑盒模型里。你做不到正常 debug,只能像侦探一样靠日志和状态快照还原现场。所以项目第一天就把工具调用的全链路日志建好,你会感谢这个决定。

至于那些看起来系统性的工作流细节,比如人类确认节点和防幻觉机制怎么设计、LangGraph 里状态恢复怎么做,实操里多盘几轮自然会有感觉。我自己的体会是:不要把模型当成队友,把它当成一个能力很强但极不稳定的实习生,外壳就是你给他配的工作流程和审批流。一切流程、约束和检查,都是为了让他稳定发挥出能力,同时犯不了大错。想清楚这个定位,很多设计决策就豁然开朗了。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进