做AI Agent的同行应该都碰过这种事:模型本身明明表现不错,可一旦让它跑一个多步骤的真实任务,比如“帮我把上周的销售数据拉出来,做个环比分析,再把结论发到工作群”,结果就经常失控。要么中间某一步拿错了参数,要么工具调用成功了但状态没更新,要么干脆卡在同一个动作里反复重试。后来我才想明白,问题基本不在模型,而在外面这层壳——也就是我们常说的AI Agent Harness Engineering 执行链路分层模型没有设计好。
所谓Harness,直译是“挽具”,放到Agent工程里就是套在模型外面那套可控制的执行环境。它管意图解析、规划、决策、工具调用这一整条链路怎么流转、怎么约束、怎么观测、怎么恢复。模型只是链路里的一个“思考部件”,真正决定系统稳不稳的,是链路的骨架。这篇文章就围绕“意图解析、规划、决策到工具调用”这条执行链路分层模型,讲清楚每一层到底该干什么、层与层之间怎么衔接,以及我实际搭建时踩过的坑和沉淀下来的可复用做法。适合正在做Agent落地的开发者,也适合想把原型变成稳定系统的团队参考。
1. 为什么需要一套显式的执行链路分层模型
很多团队第一版Agent往往是最省事的写法:一个主循环,把所有Prompt拼在一起,让模型自己“发挥”。输入进来之后,模型自己判断要做什么、自己决定用哪个工具、自己填参数。这种单体结构在小Demo上看着很灵活,但只要任务稍微复杂一点,问题就成片出现。
1.1 先厘清:Agent Harness到底管的是什么
先说清楚边界。Harness不是模型,也不是工具本身,它是模型和外部世界之间那层“胶水层”。它要干的事包括这几件:
- 把用户原始输入转成结构化意图,而不是让模型漫无边际地理解。
- 把意图拆成可执行的任务序列,规划出一组动作步骤。
- 在多个候选动作、多个工具、多个参数方案之间做选择。
- 真正去调用外部API或内部服务,然后把结果回填给下一次推理。
听起来就是四条步骤,但难点在于:每一步都可能出错,而错误会顺着链路往后传染。如果这四条步骤没有明确分层,就意味着没人对“中间状态”负责。比如模型输出的JSON不符合工具参数要求,这个错误可能直到调用工具时才暴露,而那时链路已经走了一半,回滚成本非常高。
我见过一个团队的产品,用户让Agent“整理会议纪要并发送邮件”,结果Agent先调了邮件API,发现收件人参数缺失,又回头重新理解意图。整条链路乱成一团,用户看到的就是一顿操作之后什么都没发生。这类事故的根因就是Harness没有分层,链路里缺少“意图、规划、决策、执行”各自独立的控制点。
1.2 不分层会踩哪些坑
不分层的Agent,最典型的就是把一个巨大的Prompt塞给模型,然后用“function calling”硬撑。实际问题有三个:
第一,状态不透明。整个执行过程在一个黑盒上下文里进行,模型每轮都在重新“猜”当前状态。工具调用是否成功、参数从哪来的、下一步为什么这么走,全都不可观测。出了问题只能翻原始对话记录,定位效率极低。
第二,错误恢复几乎靠运气。没有分层,就没有明确的错误边界。工具返回一个异常,模型可能误以为任务已经完成,也可能陷入同一个动作的重试死循环。我曾经在一个Demo里看到Agent反复调用同一个查询接口几十次,原因只是模型认为“多试几次总能成功”。
第三,成本和延迟不可控。不分层的Agent会把大量上下文反复传递给模型。本来只需要执行一个动作,却要重新把历史记录、工具描述、角色设定全部算一遍,Token消耗成倍增长,响应时间也跟着拉长。
所以我在后来的项目里定了一条规矩:凡是超过两个工具调用的Agent任务,必须显式地做链路分层。宁可牺牲一些“灵活”,也要换来可控制、可观测、可回滚。
2. 执行链路分层模型的整体设计
我采用的分层模型,不是简单地把四个词写在PPT上,而是每一层都有明确的输入、输出、失败处理方式和状态边界。下面把这套模型的关键设计展开说。
2.1 四层模型各层到底承担什么职责
这套执行链路分层模型,我把执行过程分成四个串联的层:
第一层是意图解析层。输入是用户原始文本(也可能是语音转写、图片识别结果),输出是一个结构化的“意图对象”,至少包含意图类型、关键实体、置信度。这一层的作用是把“人说的话”翻译成“机器可判断的指令”。没有这一层,后续规划层就无法稳定工作。
第二层是规划层。输入是意图对象,输出是一个“执行计划”,通常是一组有序或带依赖关系的步骤。每一步指定了要调用什么工具、需要什么参数模板、依赖哪些前置步骤的结果。这一层相当于把目标分解成可执行的操作清单。
第三层是决策层。它负责在规划的基础上,结合当前上下文、环境状态、可用资源,决定“具体执行哪一个候选动作”。比如两个数据源都能查天气,决策层要决定用哪个;一个工具调用有失败风险,决策层要决定是重试、换路还是终止。
第四层是工具调用层。它最靠近外部系统,负责真正执行动作。包括参数校验、鉴权、超时控制、重试策略、结果规范化。这一层还负责把执行结果转换成统一格式回填给上下文,供后续步骤使用。
这四层是串联关系,但并不是严格的单向流水线。规划结果可以因为工具执行失败而触发重新规划,决策结果也可以反馈给意图层做二次澄清。重点是,每一层都不越权,不直接处理属于别的层的问题。
2.2 层间协议:上下文流、状态机与信号
分层不是把代码拆成四个模块就完事,关键是层与层之间要有清晰的协议。我把它归纳成三点:上下文流、状态机、信号。
上下文流指每一层拿什么数据、往上下文里写什么数据。我规定:意图对象只在执行开始时写入上下文,之后不再由模型直接修改。规划生成的Plan在执行期间可以更新,但步骤完成后必须回写执行结果。工具返回的结果需要经过规范化,变成统一结构后才能进入上下文,避免模型被乱七八糟的原始响应带偏。
状态机则用来约束链路生命周期。我定义的Agent状态至少包括:idle、parsing、planning、deciding、executing、awaiting_confirm、failed、finished。每一层的入口和出口都附带一个状态迁移条件。例如只有大量意图置信度大于阈值才允许从parsing进入planning,否则进入awaiting_confirm,等待用户澄清。
信号是层间传递的控制消息,比如“需要重规划”“需要澄清”“工具调用超时”。这些信号不能由模型自由产生,而是由代码在特定条件下抛出并由对应层捕获。这样就能避免出现“模型自己决定重来一遍”这类不可控行为。
2.3 分层方案与单体Agent的对比
我用一张表总结分层和非分层的差异,方便团队内部对齐认知:
| 维度 | 单体Agent | 分层Harness |
|---|---|---|
| 状态透明度 | 模型上下文内部,隐含不透明 | 每层显式状态,随时可观测 |
| 错误定位 | 需要回溯对话记录 | 按层排查,两步定位 |
| 重试策略 | 模型自己决定,容易死循环 | 代码控制,按错误类型分类处理 |
| 成本控制 | 全链路重复计费 | 每层按需消耗,可精确统计 |
| 可测试性 | Prompt测试为主 | 每层可单独写单测和Mock |
| 扩展性 | 增加能力要改主循环 | 新增工具/策略只需要扩展对应层 |
我这里说的是工程模型上的对比,不是说单体一定不能用。早期验证想法、跑一个技术Demo,单体写法确实更快。但一旦要接生产流量、要在团队里多人维护、要对线上的失败负责,分层带来的控制力完全是两种体验。
3. 核心细节:每一层的关键机制与决策逻辑
这一节是我最想分享的部分。四层拆出来容易,但每一层内部有哪些门道,才是决定Harness是否好用的关键。
3.1 意图解析层的实体抽取与置信度控制
意图解析层在很多工程里容易被简化成一个分类任务:让模型返回一个intent名称就完事。但实际生产里,意图解析必须同时解决“实体抽取”和“置信度控制”两件事。
以“帮我查一下明天下雨吗”为例,意图类型是weather_query,实体包括城市、日期。如果只识别意图不抽实体,规划层就无从下手。我用的方案是要求模型输出严格的结构化JSON,并且用JSON Schema做校验:
{ "intent": "weather_query", "entities": { "city": {"type": "string", "value": "上海"}, "date": {"type": "date", "value": "2025-06-10"} }, "confidence": 0.92, "need_clarification": false }confidence字段是我后来强制加的。刚开始没有这个字段,模型哪怕完全没把握也会硬猜一个意图,导致后续规划层跟着跑偏。加了这个字段之后,我设定了一条硬规则:置信度低于0.7的输入,不允许进入规划层,必须走澄清流程。这里的0.7不是拍脑袋定的,而是统计了线上历史数据里意图误判的分布,发现大部分误判集中在0.5到0.7这个区间,阈值调到0.7之后误判率明显下降。
实体抽取的坑在于,模型有时会把实体值填成不规范的格式,比如把“明天”填成“明天”,而不是标准日期。我的做法是在意图解析层后面接一个轻量级的标准化模块,只负责做日期、时间、地名的格式规整,不重复做语义理解。这样职责清晰,也不会让意图解析层过重。
3.2 规划层的任务分解与重规划触发
规划层要输出一个可执行计划。我使用的是一个带依赖关系的步骤列表,用JSON表示,而不是让模型自然语言描述。关键原因在于:步骤依赖关系必须能被代码解析,只有结构化的Plan才能支持状态追踪和条件跳转。
一个典型的Plan长这样:
{ "plan_id": "plan_8f3a", "steps": [ {"id": "s1", "tool": "fetch_sales_data", "params": {"date_range": "last_week"}, "depends_on": []}, {"id": "s2", "tool": "calc_compare_ratio", "params": {"baseline": "$s1.result.previous_week"}, "depends_on": ["s1"]}, {"id": "s3", "tool": "post_to_group", "params": {"content": "$s2.result.summary"}, "depends_on": ["s2"]} ] }这里$s1.result.xxx是一个引用表达式,规划层允许步骤参数引用前置步骤的输出。这个设计让我不需要在代码里写很多硬编码的数据传递逻辑,而是统一在参数解析器里处理引用关系。
重规划触发是一个容易被忽视的点。我的经验是:不是一旦出错就重新规划,那样成本太高。我设置了三级恢复策略:
- 第一级,当前步骤重试。适用于超时、网络抖动这类瞬时错误,最多重试两次。
- 第二级,替换等价工具。适用于某个工具不可用,但另一个工具能提供同样能力。
- 第三级,基于失败信息重新规划。只有当前两步都无效,而且失败信息明确指向计划本身不合理的时候,才触发重新规划。
此外还要限制最大计划步数。我默认允许一个计划最多包含10个步骤,超过之后强制进入人工确认。这个限制能有效防止Agent在一个复杂任务里无限扩展开去,把无关的事也卷进来。
3.3 决策层的策略选择与成本约束
决策层是很多人容易忽略的层,也是最需要“抠细节”的层。规划层给出了“可以怎么做”,决策层决定“这一次具体怎么做”。
我先说一个实际场景:执行“查机票”这个动作,候选工具可能有两个,一个更快但费用高,一个便宜但响应慢。决策层要做的是根据当前任务的优先级(用户是否着急、预算是否有限)选一个。这听着简单,但如果不做显式决策层,模型每次可能作出不同的选择,用户会明显感觉系统“不稳定”。
我的做法是分层决策:优先用确定性规则,规则覆盖不了的边界情况才交给模型判断。
确定性规则的例子:
- 如果任务明确要求速度,优先选择P95延迟更低的工具。
- 如果任务涉及费用,优先选择单价低的工具。
- 如果某个工具已经连续失败两次,自动标记降级。
只有规则无法判断的时候,比如两个工具综合评分接近,才把候选列表和评分因子交给模型做一次轻量裁决。这样既保留了灵活性,又保证了大部分情况下行为可预期。
决策层还有一个重要职责是成本约束。每条Agent执行链路都要有明确的预算上限,预算可以用Token数、调用时长或金额来衡量。我在决策层维护一个“剩余预算”指标,每次选择工具时会把预估消耗和剩余预算做比较。如果剩余预算不足,决策层直接跳过模型判断,进入降级路径。这个设计让整个系统在成本失控之前就能停下来,而不是等账单出来之后才追悔。
3.4 工具调用层的注册、校验与错误恢复
工具调用层是四层里最贴近工程实现的一层。这层做不好,前面所有层的精细设计都会在最后一公里崩塌。
工具调用的第一件事是注册和校验。我倾向于把所有工具定义成统一的规格,而不允许代码里到处裸调API。每个工具的描述至少包含:
- 工具名和功能描述,供规划层生成步骤时使用。
- 参数Schema,用于校验模型生成的参数是否合法。
- 调用方式(HTTP、本地函数、消息队列等)。
- 超时时间、重试次数、幂等键规则。
参数校验一定要做。我遇到过很多次,模型倒是给出了参数,但字段名跟工具要求的不一致,或者格式不对。如果不在调用前校验,错误就会留在工具内部,导致排错困难。我在工具调用层统一用JSON Schema做校验,校验失败直接返回标准化错误,而不是把错误堆栈抛给上层。
错误恢复这块,我给每个工具定义了四类错误码:
| 错误码 | 含义 | 恢复动作 |
|---|---|---|
| TIMEOUT | 调用超时 | 重试一次,仍失败则换等价工具 |
| INVALID_PARAMS | 参数校验失败 | 回到决策层修正参数 |
| AUTH_FAILED | 鉴权失败 | 不重试,进入人工处理 |
| REMOTE_ERROR | 上游服务异常 | 记录现场,按退避策略重试 |
工具执行成功之后,结果需要规范化。我规定工具返回值必须转成一个统一结构,至少包含content、metadata、status。不会把原始HTTP响应直接塞进上下文,因为那里面往往有大量无关字段,白白消耗Token,还可能把模型带偏。
4. 从零搭建一个最小可运行的Agent Harness
理论讲完,直接上实操。我提供一个最小可运行的骨架,代码是Python,逻辑清晰,可以在这个基础上扩展。
4.1 骨架:数据结构和状态流转
先定义最核心的数据结构。我用dataclass来表示意图、计划、步骤和工具规格:
from dataclasses import dataclass, field from enum import Enum from typing import Any, Optional class StepStatus(Enum): PENDING = "pending" RUNNING = "running" SUCCEEDED = "succeeded" FAILED = "failed" @dataclass class Intent: intent: str entities: dict confidence: float @dataclass class Step: id: str tool: str params: dict depends_on: list = field(default_factory=list) status: StepStatus = StepStatus.PENDING result: Optional[dict] = None @dataclass class Plan: plan_id: str steps: list[Step] max_steps: int = 10这里的关键设计是Step里的params保留原始引用表达式,比如"$s1.result.previous_week"。执行的时候再解析引用,而不是创建计划时就把值填死,这样重规划和重新执行时更灵活。
状态流转我用一个简单的HarnessState控制:
class HarnessState(Enum): PARSING = "parsing" PLANNING = "planning" DECIDING = "deciding" EXECUTING = "executing" AWAITING_CONFIRM = "awaiting_confirm" FINISHED = "finished" FAILED = "failed"状态不是装饰,我会在链路的每个关键点显式检查当前状态是否满足转移条件。比如只能在PARSING状态下进入PLANNING,避免代码改混之后状态乱跳。
4.2 把四个层串起来:执行入口
下面这一段是Harness主流程的核心循环。我刻意写得精简,突出四层之间的衔接关系:
class AgentHarness: def __init__(self, intent_parser, planner, decider, tool_registry): self.intent_parser = intent_parser self.planner = planner self.decider = decider self.tool_registry = tool_registry self.state = HarnessState.PARSING def run(self, user_input: str): intent = self.intent_parser.parse(user_input) if intent.confidence < 0.7: self.state = HarnessState.AWAITING_CONFIRM return {"status": "need_clarification", "message": "我没有完全理解你的意思,请补充信息"} self.state = HarnessState.PLANNING plan = self.planner.create_plan(intent) if not plan.steps: self.state = HarnessState.FAILED return {"status": "failed", "reason": "empty_plan"} self.state = HarnessState.DECIDING plan = self.decider.apply_policy(plan) self.state = HarnessState.EXECUTING result = self._execute_plan(plan) self.state = HarnessState.FINISHED return {"status": "finished", "result": result} def _execute_plan(self, plan: Plan): for step in plan.steps: if step.status == StepStatus.SUCCEEDED: continue resolved_params = self._resolve_params(step.params, plan.steps) tool = self.tool_registry.get(step.tool) try: step.status = StepStatus.RUNNING step.result = tool.execute(**resolved_params) step.status = StepStatus.SUCCEEDED except ToolExecutionError as e: step.status = StepStatus.FAILED if self.decider.should_retry(e, attempt=1): step.status = StepStatus.PENDING step.result = tool.execute(**resolved_params) step.status = StepStatus.SUCCEEDED else: return {"error": str(e), "step": step.id} return {"steps": [{"id": s.id, "status": s.status.value} for s in plan.steps]}这个主循环看起来简单,但它已经具备四层模型的核心:意图解析的置信度门槛、规划生成、决策策略、工具调用的重试。真正的生产系统会比这复杂得多,但骨架就是这个样子。
_resolve_params负责解析$s1.result.xxx引用:
def _resolve_params(self, params: dict, steps: list[Step]) -> dict: resolved = {} for key, value in params.items(): if isinstance(value, str) and value.startswith("$"): ref_path = value[1:].split(".") step_id = ref_path[0] field_path = ref_path[2:] step = next(s for s in steps if s.id == step_id) data = step.result for field in field_path: data = data[field] resolved[key] = data else: resolved[key] = value return resolved这里我默认引用路径格式是$步骤id.result.字段名,你也可以扩展成支持嵌套字段,实现成本不高。
4.3 可观测性:链路ID与关键埋点
没有可观测性的Harness等于没有仪表盘的飞机。我在搭建骨架时就把链路ID加上了,从入口请求开始生成一个trace_id,整条链路传递下去。
实践做法是:每个层开始时打一条结构化日志,包含trace_id、当前状态、耗时、关键字段摘要。比如:
trace_id=7f2a state=parsing input_len=18 intent=weather_query confidence=0.92 trace_id=7f2a state=planning steps=3 plan_id=plan_8f3a trace_id=7f2a state=executing tool=fetch_sales_data cost_ms=120 status=ok这些日志的价值在于排障。一次故障发生之后,不需要去翻模型原始输出,只需按trace_id把所有日志拉出来,就能看到完整链路,定位是决策层选错了工具,还是工具调用层参数传错了。这个体验,对比单体Agent只能“看对话记录猜”,完全是两个时代。
5. 常见问题与排查技巧实录
这一节把我实际线上遇到的高频问题整理成清单,每条都配套排查思路,希望能帮你少走弯路。
5.1 高频翻车现场
先说意图误判。用户输入“帮我看看明天要不要带伞”,模型返回的意图是travel_query,而不是weather_query。问题出在意图解析层的提示词里缺少生活化表达示例。加入更多口语化few-shot示例之后,这类误判明显减少。注意,意图解析的few-shot不是随便写几个例句,而是要覆盖目标用户日常最常用的说法。
再说规划死循环。计划里出现两个步骤互相依赖,比如步骤A依赖B的结果,B又依赖A的结果,模型生成这样的计划之后,执行器就永远等不到前置步骤完成。我后来在规划层加了一个拓扑检查,执行前先判依赖关系里有没有环,有环就直接拒绝该计划并触发重规划。
还有决策抖动。同一个输入,决策层在几个等价工具之间反复横跳,比如有时选第一数据源,有时选第二数据源,测试人员认为系统不稳定。解决方法是把决策的优先规则写死:默认按价格排序,只有价格接近时才把请求交给模型判断。这样大部分情况下行为可预期,只有边界情况才消耗模型能力。
最后是工具返回解析失败。工具返回了一个合法JSON,但结构跟预期不一致,比如预期是{"data": {...}},实际返回{"data": [...], "meta": {...}}。这既可能是工具方需求变化,也可能是规范化层的Schema定义太严格。我的建议是Schema只约束核心字段,业务上的灵活字段单独放,不要一把锁死,给扩展留空间。
5.2 问题定位速查表
| 现象描述 | 可能原因 | 优先排查点 |
|---|---|---|
| Agent答非所问 | 意图解析置信度偏高质量差 | 查看意图解析日志,确认intent和实体 |
| 计划一直不执行 | 步骤依赖不满足或存在环 | 检查Plan里depends_on字段 |
| 每次结果都不一样 | 决策层缺少确定性规则 | 查看决策层的候选排序逻辑 |
| 工具调用一直超时 | 超时设置太小或网络抖动 | 查看错误码和耗时统计 |
| 上下文越来越大 | 工具结果未规范化回填 | 检查工具调用的result清洗逻辑 |
| 模型输出非法JSON | 提示词格式约束不足 | 在解析层增加JSON语法校验和纠错 |
排查的核心思路是“先用日志定位到层,再下钻看细节”。不要在刚开始排查时就去读完整对话记录,那样效率太低。先看是哪一层出了问题,再针对那层的输入输出做分析,基本能把90%的问题快速收敛。
6. 一些真实的工程体会
我在把这套分层模型落地到不同项目之后,最深的一点体会是:分层不是为了“显得专业”,而是为了在系统出问题时还有控制权。模型本身是概率系统,一定会出错,Harness的价值就是让错误被限制在某个层里,而不是让错误顺着链路自由传播。
有两点建议送给大家。
第一,不要一上来就做复杂的编排框架。先用我上面这种最小的四层骨架跑通一个真实场景,再逐步加能力。很多团队一上来就追求支持几十种工具、几十种错误策略,结果主循环复杂到没人敢改,反而把项目拖垮。
第二,一定要给每一层配日志。没有日志的分层模型是空中楼阁。哪怕代码写得再清晰,线上出了问题无法定位,一切设计都是零。我习惯把“分层日志”当成Harness的一部分来写,而不是事后补。
另外,关于Agent的未来,我个人的习惯是持续关注执行链路这一层的工程化进展。模型能力会越来越强,但工程壳的作用不会消失,反而会越来越重要。谁能把意图解析、规划、决策、工具调用这四段链路控制得干净利落,谁的产品体验就能稳定领先。希望这篇文章能给你一些可落地的思路。