☰
AI Agent Harness工程实战:从能跑到能扛的稳定性设计
2026/10/9 1:22:07 网站建设 项目流程

1. 为什么“能跑”的 Agent 和“能扛”的 Agent 是两回事

我见过太多团队在 Demo 阶段兴奋不已,Agent 在本地跑通了,工具调用顺畅,多轮对话也没问题,然后信心满满地推到线上。结果呢?并发一上来,工具调用开始超时,上下文窗口被撑爆,模型返回格式飘忽不定,整个链路像多米诺骨牌一样倒下去。这不是模型不行,而是Harness 工程没做到位。

先把这个概念说清楚。所谓 Harness,直译是“马具”或“束具”,在 AI Agent 语境下,它指的是包裹在模型外面的一整套工程化控制层。模型本身是一个概率性的文本生成器,它不知道什么时候该调用工具、什么时候该停止、输出格式对不对、上下文有没有超限。Harness 就是那个“缰绳”,负责把模型的原始输出约束成可预测、可调度、可观测的系统行为。

很多人把 Harness 和 Agent 框架混为一谈。LangChain、LangGraph、Spring AI 这些是框架,它们提供了构建 Agent 的脚手架;而 Harness 是你在这个脚手架上搭建的运行时控制逻辑。框架解决的是“怎么搭”,Harness 解决的是“怎么稳”。一个 Agent 能跑通,靠的是框架;一个 Agent 能扛住线上流量,靠的是 Harness。

我自己的经验是,Harness 工程至少包含五个核心机制:输入输出的结构化约束、工具调用的生命周期管理、上下文窗口的动态调度、错误恢复与重试策略、可观测性与回放能力。这五个机制缺一个,Agent 在线上就会变成一颗定时炸弹。接下来的内容,我会逐个拆解这些机制的设计思路和实操细节,同时穿插我在实际项目中踩过的坑和总结出来的经验。

提示:如果你现在还在 Demo 阶段,建议先把 Harness 的基本骨架搭起来,不要等到上线前才补。后期补 Harness 的成本远高于前期设计。

2. 结构化约束:让模型的输出从“散文”变成“合同”

2.1 为什么格式约束是 Harness 的第一道防线

模型输出本质上是自然语言,而自然语言是模糊的、多义的、不稳定的。你让模型返回一个 JSON,它可能给你返回一个带 Markdown 代码块的 JSON,可能给你返回一个字段名大小写不一致的 JSON,甚至可能给你返回一段“好的,以下是 JSON 格式的结果”然后才跟 JSON。这些在 Demo 阶段你可能手动处理一下就过去了,但在线上,每一次格式异常都意味着一次解析失败,一次解析失败就可能触发一次重试,重试又消耗 token 和时间,连锁反应下来,整个系统的吞吐量会被拖垮。

结构化约束的核心思路是:不要让模型“自由发挥”,而是用 schema 把它的输出空间压缩到最小。具体做法包括三个层次。

第一个层次是Prompt 层面的格式指令。你需要在系统提示词里明确告诉模型:输出必须是一个合法的 JSON 对象,不要包含任何额外的解释文字,不要使用 Markdown 代码块包裹,字段名必须严格遵循给定的 schema。这一步能解决大部分格式问题,但解决不了全部。

第二个层次是API 层面的结构化输出。现在主流的大模型 API 都支持某种形式的结构化输出,比如 JSON mode、Function Calling、Tool Use 等。以 OpenAI 的 Function Calling 为例,你可以定义一个函数的参数 schema,模型会按照这个 schema 来生成参数。这比纯 Prompt 约束要可靠得多,因为模型在解码阶段就受到了约束。

第三个层次是应用层面的校验与修复。即使前两层都做了,仍然会有少量输出不符合预期。这时候你需要一个校验层,用 JSON Schema 校验器(比如 Python 的jsonschema库)来验证输出,如果校验失败,触发一次修复重试。修复重试的策略后面会详细讲。

2.2 实操中的 schema 设计原则

我在设计 schema 时总结了几个原则,都是从踩坑中得来的。

原则一:字段尽量扁平,避免深层嵌套。模型对深层嵌套结构的生成准确率明显低于扁平结构。如果你需要嵌套,尽量控制在两层以内。比如{"action": "search", "params": {"query": "..."}}这种两层结构是可以接受的,但如果你嵌套到四层五层,模型出错的概率会急剧上升。

原则二:枚举值优于自由文本。如果一个字段的取值是有限的几个选项,一定要用 enum 约束。比如"status": {"type": "string", "enum": ["success", "failed", "pending"]}。这样模型就不会给你返回“成功了”“失败了呢”“还在处理中”这种五花八门的表达。

原则三:必填字段尽量少。每增加一个必填字段,模型出错的概率就增加一分。把非核心字段设为可选,让模型在不确定时可以省略,而不是强行编造一个值。

原则四:给字段加描述。JSON Schema 的description字段不仅是给开发者看的,很多模型在生成时也会参考这个描述。一个好的描述能显著提升字段填充的准确率。比如"query": {"type": "string", "description": "用户想要搜索的关键词,不要包含任何修饰词或标点符号"}。

下面是一个我在实际项目中使用的 schema 示例,用于一个工具调用场景:

{ "type": "object", "properties": { "tool_name": { "type": "string", "enum": ["search", "calculate", "summarize", "translate"], "description": "要调用的工具名称" }, "tool_params": { "type": "object", "properties": { "input_text": { "type": "string", "description": "工具的输入文本" }, "options": { "type": "object", "description": "可选参数,不同工具支持不同的选项" } }, "required": ["input_text"] }, "reasoning": { "type": "string", "description": "选择该工具的原因,用于调试和可观测性" } }, "required": ["tool_name", "tool_params"] }

注意reasoning字段是可选的,但我在实际使用中发现,加上这个字段后,模型的工具选择准确率有明显提升。因为模型在生成reasoning的过程中,相当于做了一次“思维链”,它会先解释为什么选这个工具,然后再生成工具名和参数,这个顺序上的“自我解释”能减少随机性。

2.3 校验失败后的修复策略

校验失败后的处理方式,直接决定了系统的稳定性。我见过两种极端做法:一种是直接抛异常,让上层重试整个请求;另一种是忽略校验失败,把原始输出硬塞给下游。这两种都不可取。

我的做法是分三级处理。第一级是自动修复:如果校验失败的原因是格式问题(比如多了 Markdown 代码块、字段名大小写不一致),用一个轻量的修复函数自动处理,不消耗模型调用。第二级是模型修复:如果自动修复搞不定,把校验错误信息和原始输出一起发给模型,让它重新生成一次,这次在 Prompt 里明确带上错误信息。第三级是降级处理:如果模型修复也失败,走降级逻辑,比如返回一个默认值、或者把请求转给人工处理队列。

这里有个关键细节:修复重试一定要有次数上限。我一般设置最多两次修复重试,超过两次就降级。因为如果模型连续两次都生成不对,说明要么 Prompt 有问题,要么这个请求本身就不适合当前模型处理,继续重试只是浪费资源。

3. 工具调用的生命周期:从“裸调”到“全链路管控”

3.1 工具调用为什么容易成为稳定性瓶颈

工具调用是 Agent 区别于普通聊天机器人的核心能力,但也是稳定性问题的高发区。一个工具调用涉及至少五个环节:模型决定调用工具、模型生成调用参数、Harness 解析调用请求、实际执行工具、把执行结果返回给模型。这五个环节中任何一个出问题,都会导致整个调用链失败。

我遇到过的典型问题包括:模型生成了不存在的工具名、参数类型不对(比如该传数字传了字符串)、工具执行超时、工具返回的结果太大撑爆上下文、工具执行报错但模型不知道如何处理。这些问题在 Demo 阶段可能偶尔出现,但在线上高并发场景下,每一个都会被放大。

3.2 工具注册与发现机制的设计

Harness 需要维护一个工具注册表,记录每个工具的元信息:名称、描述、参数 schema、超时时间、重试策略、限流配置。这个注册表是 Harness 的核心数据结构之一。

工具注册表的设计要点有几个。第一,工具描述要面向模型优化。很多人写工具描述是给开发者看的,用了很多技术术语,但模型看不懂。工具描述应该用自然语言,清晰说明这个工具做什么、什么时候用、输入输出是什么。比如不要写“执行 HTTP GET 请求”,而要写“根据给定的 URL 获取网页内容,适用于需要从互联网获取信息的场景”。

第二,参数 schema 要严格。和前面说的结构化约束一样,工具参数的 schema 也要尽量扁平、用枚举、加描述。第三,每个工具要有独立的超时和重试配置。搜索工具可能需要 10 秒超时,而计算工具 1 秒就够了。不要用一个全局超时配置一刀切。

下面是一个工具注册表的配置示例:

TOOL_REGISTRY = { "search": { "description": "根据关键词搜索互联网上的信息,返回相关网页的摘要", "parameters": { "type": "object", "properties": { "query": {"type": "string", "description": "搜索关键词"}, "max_results": {"type": "integer", "default": 5, "maximum": 10} }, "required": ["query"] }, "timeout_seconds": 10, "max_retries": 2, "rate_limit": "100/minute" }, "calculate": { "description": "执行数学计算,支持加减乘除和常见数学函数", "parameters": { "type": "object", "properties": { "expression": {"type": "string", "description": "数学表达式,如 '2 + 3 * 4'"} }, "required": ["expression"] }, "timeout_seconds": 2, "max_retries": 1, "rate_limit": "1000/minute" } }

3.3 工具执行结果的截断与摘要

工具返回的结果往往很大,比如搜索工具返回了十个网页的全文,或者数据库查询返回了几百行数据。这些结果如果直接塞回给模型,会迅速撑爆上下文窗口。所以 Harness 必须有一个结果处理层,负责截断、摘要或结构化压缩。

我的做法是分场景处理。对于搜索结果,只保留每条结果的标题和前 200 个字符的摘要,最多保留 5 条。对于结构化数据,如果行数超过阈值,只保留前 N 行,并附加一个“共 X 行,已截断”的提示。对于文本内容,如果超过 token 限制,用一个轻量的摘要模型先做一次压缩,再把压缩后的结果返回给主模型。

这里有个容易忽略的点:截断后的结果要告诉模型这是截断后的。否则模型可能会基于不完整的信息做出错误判断。我一般会在截断结果后面加一句“注意:以上结果已截断,仅展示前 N 条”。

3.4 工具调用失败的降级路径

工具调用失败是常态,不是异常。网络抖动、第三方服务限流、参数不合法,这些都可能导致工具调用失败。Harness 需要为每种失败情况设计降级路径。

我的降级策略是这样的:超时失败,重试一次,如果还超时,返回一个“工具暂时不可用”的提示给模型,让模型决定是换一个工具还是直接告诉用户。参数错误,把错误信息返回给模型,让模型重新生成参数,最多重试两次。服务端错误,根据错误码决定是否重试,5xx 错误重试,4xx 错误不重试直接降级。限流错误,等待一段时间后重试,或者切换到备用工具。

关键原则是:不要让工具调用失败直接导致整个 Agent 请求失败。Agent 应该有能力在工具不可用时,仍然给用户一个合理的回复,哪怕这个回复是“抱歉,我暂时无法获取相关信息”。

4. 上下文窗口的动态调度:别让“记忆”变成“负担”

4.1 上下文膨胀的根因分析

Agent 的上下文窗口是有限资源,但很多设计会让它快速膨胀。多轮对话的历史消息、每次工具调用的请求和结果、系统提示词、few-shot 示例,这些加起来很容易就超过模型的上下文限制。一旦超限,要么请求被拒绝,要么模型开始“遗忘”早期内容,导致行为不一致。

我见过一个典型的反面案例:一个客服 Agent 把用户的所有历史对话都塞进上下文,结果跑到第十轮的时候,上下文已经超过 100K token,响应时间从 2 秒涨到 15 秒,而且模型开始重复之前已经问过的问题。这就是典型的上下文管理缺失。

4.2 分层上下文管理策略

我的做法是把上下文分成四层,每层有不同的保留策略。

第一层是系统提示词和工具定义,这是固定不变的,永远保留。第二层是最近 N 轮对话,这是短期记忆,完整保留。N 的取值根据模型上下文窗口大小和平均消息长度来定,一般取 5 到 10 轮。第三层是更早的对话摘要,把早期对话压缩成一段摘要,保留关键信息。第四层是工具调用记录,只保留最近几次的工具调用详情,更早的只保留调用摘要。

这个分层策略的核心思想是:信息的价值随时间衰减,但衰减速度不同。系统提示词永远有价值,最近对话价值最高,早期对话只有摘要价值,工具调用记录只有最近几次有参考价值。

4.3 摘要压缩的触发时机与实现

摘要压缩不是每轮都做,那样太浪费资源。我的触发条件是:当上下文 token 数超过模型窗口的 70% 时,触发一次压缩。压缩的对象是第二层中最早的那部分对话,把它们合并到第三层的摘要中。

压缩的实现方式有两种。一种是用模型做摘要,把要压缩的对话发给一个轻量模型,让它生成一段摘要。这种方式的优点是摘要质量高,缺点是增加了一次模型调用,有延迟和成本。另一种是用规则做摘要,比如只保留用户的问题和 Agent 的最终回答,去掉中间的思考过程和工具调用细节。这种方式快且免费,但摘要质量取决于规则设计。

我一般用混合方式:对于关键对话(比如涉及用户核心诉求的),用模型做摘要;对于普通对话,用规则做摘要。这样在质量和成本之间取得平衡。

4.4 上下文窗口的监控与告警

上下文管理不能靠感觉,要有数据支撑。我会在 Harness 里埋点,记录每次请求的上下文 token 数、各层占比、压缩触发次数。当发现上下文 token 数持续接近上限,或者压缩触发频率异常升高时,触发告警。

这些数据还能帮助优化 Prompt 设计。比如如果发现系统提示词占了太多 token,可以考虑精简;如果发现工具调用结果经常很大,可以考虑优化工具的输出格式。

5. 错误恢复与重试:把“意外”变成“预期”

5.1 错误分类与差异化处理

错误恢复的第一步是错误分类。不同类型的错误需要不同的处理策略,一刀切的重试只会浪费资源。

我把错误分成四类。瞬时错误:网络抖动、临时限流,这类错误重试就能解决。参数错误:模型生成的参数不合法,这类错误需要让模型重新生成,而不是简单重试。逻辑错误:模型选择了错误的工具或做出了错误的决策,这类错误需要调整 Prompt 或增加约束。系统错误:代码 bug、配置错误,这类错误重试没用,需要人工介入。

5.2 重试策略的设计与参数选择

重试策略的核心参数有三个:重试次数、重试间隔、退避策略。

重试次数我一般设置为 2 到 3 次。太少了覆盖不了瞬时错误,太多了浪费资源且增加延迟。重试间隔用指数退避,第一次间隔 1 秒,第二次 2 秒,第三次 4 秒。这样既能给下游服务恢复的时间,又不会让请求等待太久。退避策略要加随机抖动,避免多个请求同时重试造成“惊群效应”。

这里有个细节:重试要有幂等性保证。如果工具调用不是幂等的(比如“发送消息”这种操作),重试可能导致重复执行。对于非幂等操作,要么不重试,要么在重试前先检查上一次是否已经成功。

5.3 熔断与降级的触发条件

当某个工具或某个下游服务的错误率超过阈值时,Harness 应该触发熔断,暂时停止调用该工具,直接走降级路径。熔断的阈值我一般设置为:1 分钟内错误率超过 50%,且请求数超过 10 次。熔断后进入半开状态,每隔 30 秒放一个请求过去试探,如果成功则恢复,失败则继续熔断。

降级路径的设计要提前做好。比如搜索工具熔断了,降级路径可能是“使用缓存结果”或“告诉用户暂时无法搜索”。降级不是失败,而是有策略地放弃部分功能,保证核心功能可用。

5.4 从错误中学习的回放机制

每次错误都是一次学习机会。我会把失败的请求完整记录下来,包括输入、上下文、模型输出、工具调用、错误信息。然后定期分析这些失败案例,找出共性原因,优化 Prompt、schema 或工具设计。

这个回放机制的价值在于:它把偶发的线上问题变成了可复现的测试用例。每次优化后,用这些历史失败案例做回归测试,确保优化真的解决了问题,而不是引入了新的问题。

6. 可观测性:看不见的 Agent 就是不可控的 Agent

6.1 Agent 可观测性的三个维度

Agent 的可观测性和传统服务不同,它需要覆盖三个维度:模型维度、工具维度、业务维度。

模型维度关注的是模型的输入输出、token 消耗、响应时间、格式合规率。工具维度关注的是工具调用次数、成功率、平均耗时、错误分布。业务维度关注的是用户满意度、任务完成率、多轮对话轮数。这三个维度缺一不可,只看模型维度会忽略工具问题,只看工具维度会忽略模型问题,只看业务维度则无法定位根因。

6.2 关键指标的埋点与采集

我在 Harness 里埋了这些关键指标:

指标名称类型说明
request_totalCounter总请求数
request_duration_secondsHistogram请求耗时分布
model_tokens_totalCounter模型 token 消耗总量
model_format_error_totalCounter格式校验失败次数
tool_call_totalCounter工具调用总次数
tool_call_error_totalCounter工具调用失败次数
tool_call_duration_secondsHistogram工具调用耗时分布
context_tokensGauge当前上下文 token 数
retry_totalCounter重试次数
circuit_breaker_stateGauge熔断器状态

这些指标用 Prometheus 格式暴露,配合 Grafana 做可视化。关键告警包括:格式校验失败率超过 5%、工具调用失败率超过 10%、平均请求耗时超过 10 秒、上下文 token 数超过窗口的 90%。

6.3 链路追踪在 Agent 场景下的特殊处理

传统的链路追踪是请求级别的,但 Agent 的一个请求内部可能包含多次模型调用和工具调用,需要更细粒度的追踪。我的做法是给每个 Agent 请求分配一个 trace_id,每次模型调用和工具调用分配一个 span_id,形成树状结构。

这样当用户反馈“这次回答不对”时,我可以根据 trace_id 找到完整的调用链,看到模型每次的输入输出、工具每次的调用结果,快速定位问题出在哪一环。

6.4 日志规范与敏感信息脱敏

Agent 的日志里可能包含用户隐私信息、API 密钥、内部数据。日志规范要明确哪些字段必须脱敏、哪些字段必须记录、哪些字段禁止记录。

我的做法是:用户输入和模型输出记录摘要(前 100 字符),工具调用的参数记录结构但不记录具体值(除非是公开数据),API 密钥和 token 绝对不记录。所有日志在写入前经过一个脱敏过滤器,确保敏感信息不会泄露。

7. 从单机到集群:并发场景下的 Harness 调优

7.1 并发瓶颈的定位方法

当 Agent 从单机部署扩展到集群时,并发问题会集中暴露。定位并发瓶颈的方法和传统服务类似:压测、监控、分析。但 Agent 场景有几个特殊点。

模型 API 的限流是最大瓶颈。大多数模型 API 都有 RPM(每分钟请求数)和 TPM(每分钟 token 数)限制。当并发请求数超过限制时,请求会被拒绝或排队。Harness 需要感知这些限制,做好请求排队和限流。

工具调用的并发能力参差不齐。有些工具是本地计算,并发能力很强;有些工具依赖第三方服务,并发能力有限。Harness 需要为每个工具配置独立的并发限制。

上下文管理在并发下更复杂。多个请求同时读写上下文存储时,需要处理并发一致性问题。

7.2 请求队列与优先级调度

我的做法是在 Harness 和模型 API 之间加一个请求队列。所有模型调用请求先进入队列,然后由调度器按照优先级和限流配置逐个发送。

优先级的设计根据业务场景来定。比如实时对话请求优先级高,后台批量处理请求优先级低。当队列积压时,低优先级请求可以被延迟或丢弃,保证高优先级请求的响应时间。

队列的实现可以用 Redis 或者内存队列。关键是队列要有持久化能力,避免服务重启时丢失请求。

7.3 状态管理与会话一致性

Agent 是有状态的,多轮对话需要保持会话一致性。在集群环境下,同一个会话的多个请求可能被分配到不同的实例上,这就需要把会话状态外置到共享存储(比如 Redis)。

会话状态包括:对话历史、上下文摘要、工具调用记录、会话元信息。这些状态需要设置合理的过期时间,避免无限增长。我一般设置会话过期时间为 30 分钟,超过 30 分钟没有新请求就清理。

并发读写会话状态时,需要加锁或者用乐观并发控制。我一般用 Redis 的 WATCH/MULTI 实现乐观锁,冲突时重试。

7.4 压测方案与容量规划

压测是容量规划的基础。Agent 的压测和传统服务不同,不能只压 QPS,还要模拟真实的对话模式:多轮对话、工具调用、上下文增长。

我的压测方案是这样的:用 Locust 或者 k6 模拟用户,每个用户执行一个多轮对话脚本,脚本里包含工具调用和上下文增长。压测指标除了 QPS 和延迟,还要看 token 消耗速率、工具调用成功率、上下文压缩触发频率。

根据压测结果做容量规划:如果单实例能支撑 50 QPS,目标峰值是 500 QPS,那就需要至少 10 个实例,再留 20% 的余量,实际部署 12 个实例。

8. 一些踩坑之后的个人体会

Harness 工程不是一次性设计出来的,而是在一次次线上问题中打磨出来的。我最初做 Agent 的时候,也觉得 Prompt 写好了、工具接上了就行了,结果线上各种问题打得我措手不及。后来才慢慢意识到,Agent 的稳定性不取决于模型有多强,而取决于 Harness 有多厚。

如果让我给正在做 Agent 的同行一个建议,我会说:先把 Harness 的骨架搭起来,哪怕功能少一点,也要保证每个环节都是可控的。结构化约束、工具生命周期管理、上下文调度、错误恢复、可观测性,这五件事一件都不能省。省了任何一件,后面都要用加倍的时间来补。

还有一个体会是:不要追求一次设计完美。Harness 是演进的,先解决最痛的问题,再逐步完善。我现在的 Harness 已经迭代了十几个版本,每个版本都是被具体的线上问题驱动的。这种问题驱动的方式,比一开始就设计一个大而全的架构要有效得多。

最后分享一个小技巧:把每次线上问题都变成一个自动化测试用例。我现在有一个测试集,里面全是历史上出过问题的请求。每次改 Harness 代码,先跑这个测试集,确保没有回归。这个习惯帮我避免了很多次“修一个 bug 引入两个 bug”的情况。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询