ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

AI Agent Harness工程实战:上下文管理与并发调度机制

AI Agent Harness工程实战:上下文管理与并发调度机制 1. 从“能跑”到“跑得稳”AI Agent 的 Harness 工程到底在解决什么很多人第一次搭 AI Agent 的时候关注点几乎全在模型能力上——用哪个大模型、提示词怎么写、工具怎么接。等真正把 Agent 放到生产环境里跑上一周才会发现真正让人头疼的根本不是模型聪不聪明而是整个执行框架稳不稳。这个执行框架就是我现在越来越重视的Harness 工程。先把概念说清楚。Harness 这个词直译是“马具、挽具”放在 AI Agent 语境里它指的是包裹在模型外面的一整套运行时框架负责把用户输入组装成模型能理解的上下文负责调度模型和工具之间的来回调用负责管理多轮对话的状态负责处理超时、重试、异常负责把整个执行过程记录下来以便排查。你可以把大模型想象成一台马力很强但脾气不定的发动机Harness 就是那套传动系统、冷却系统和仪表盘——发动机再猛没有这套东西车也上不了路。我见过太多团队Demo 阶段惊艳全场一上量就各种翻车上下文越滚越长导致响应越来越慢、工具调用失败后整个流程卡死、并发一上来就出现状态串台、同一个任务重试三次结果三次结果都不一样。这些问题几乎都不是模型的问题而是 Harness 层没设计好。所以这篇文章我想系统聊聊一个稳定的 AI Agent它的 Harness 工程到底应该包含哪些核心机制以及我在实际搭建过程中踩过的坑和总结出来的实践思路。这篇文章适合两类人看一类是正在从零搭建 AI Agent、想知道工程化到底要做哪些事的开发者另一类是 Agent 已经能跑、但被稳定性和并发问题折磨得够呛的工程师。我会尽量把每个机制背后的“为什么”讲透而不是只丢一堆结论因为 Harness 这东西不理解原理照抄配置换个场景就废。2. Harness 与 Agent 的边界先搞清楚谁该干什么2.1 Harness 和 Agent 到底是不是一回事网上经常有人问“harness 和 agent 区别”这个问题其实问到了点子上。我的理解是Agent 是“做什么”Harness 是“怎么把它做稳”。Agent 描述的是一个智能体的目标、能力和决策逻辑比如“你是一个能查数据库、能发邮件、能写报告的助理”Harness 则是承载这套逻辑的运行时它不关心你具体要完成什么业务它关心的是每一次模型调用怎么发出去、结果怎么收回来、状态怎么存、错了怎么办。打个比方Agent 像是餐厅的菜谱和厨师Harness 像是厨房的动线设计、出餐流程和食品安全管理。菜谱再好厨房乱成一锅粥客人照样吃不上饭。很多开源框架其实把这两层揉在一起了用起来方便但一旦要深度定制或者排查问题就会发现边界模糊带来的痛苦——你分不清一个 bug 是业务逻辑写错了还是运行时框架本身的问题。2.2 为什么要把 Harness 单独拎出来做工程化把 Harness 当成一个独立的工程层来对待最大的好处是可替换、可观测、可复用。模型可以换从这家换到那家工具可以加今天接搜索明天接数据库但 Harness 这一层的核心机制是相对稳定的。你把它做扎实了上层业务怎么变都不慌。我自己的项目里Harness 层大概承担了这么几件事上下文组装与裁剪、模型调用的重试与降级、工具调用的参数校验与结果解析、多轮状态的持久化、并发任务的隔离与调度、全链路的日志与追踪。这些事情有一个共同点——它们都跟具体业务无关但每一个做不好都会直接导致线上事故。所以我的建议是从项目一开始就把这层单独抽象出来哪怕初期只是几个函数也比全部塞在业务代码里强。2.3 一个常见的认知误区很多人觉得 Harness 就是“胶水代码”随便写写就行。我一开始也这么想直到有一次线上出现了一个诡异的问题同一个用户连续发了两条消息第二条的回复里居然带出了第一条还没处理完的中间状态。排查了半天才发现是并发场景下上下文对象被共享了两个请求互相污染。这种问题在 Demo 阶段永远遇不到只有真正上量了才会暴露。所以 Harness 工程的核心价值恰恰体现在那些“平时看不出、出事就要命”的地方。它不是锦上添花而是雪中送炭。下面我就按几个核心机制一个个拆开讲。3. 上下文管理Agent 稳定性的第一道命门3.1 上下文为什么会失控上下文管理是 Harness 里最容易被低估、也最容易出问题的一环。大模型的上下文窗口是有限的而 Agent 的多轮对话、工具调用结果、系统提示、历史记忆全都要塞进去。如果不加管理上下文会像滚雪球一样越滚越大最后要么超出窗口被截断要么响应慢得让人抓狂要么因为塞了太多无关信息导致模型“注意力涣散”回答质量断崖式下跌。我实测过一个客服场景的 Agent不做任何上下文管理的情况下跑到第 15 轮左右单次请求的 token 数就突破了 8000响应时间从最初的 1.5 秒涨到了 6 秒以上而且模型开始频繁“忘记”前面说过的关键信息。这不是模型不行是我没给它一个干净的上下文。3.2 分层上下文的设计思路我的做法是把上下文分成几层来管理每层有不同的生命周期和裁剪策略系统层系统提示词、角色设定、全局约束。这层基本不变永远保留但要尽量精简能一句话说清就别写三段。记忆层跨会话的长期记忆比如用户偏好、历史关键结论。这层需要做摘要和检索不能全量塞。会话层当前这轮对话的完整历史。这层是裁剪的重点通常保留最近 N 轮更早的做摘要压缩。临时层当前这次工具调用的中间结果。用完即弃绝不带到下一轮。这样分层之后每一层该保留多少、该什么时候清理就有了明确的规则而不是拍脑袋决定。3.3 上下文裁剪的具体策略与参数裁剪策略我一般用组合拳。首先是滑动窗口保留最近 N 轮对话N 的取值要看业务客服类一般 6 到 10 轮够用复杂任务类可能要 15 轮以上。其次是摘要压缩把窗口之外的历史用模型总结成一段简短摘要附在系统提示后面。这里有个坑摘要本身也要消耗 token而且摘要质量不稳定所以我的经验是摘要只保留“结论性信息”比如用户已经确认的需求、已经排除的选项不要保留过程性描述。还有一个技巧是按 token 预算动态裁剪。我会给上下文设一个总预算比如 6000 token然后按优先级分配系统层固定占 800记忆层最多 1000剩下的留给会话层和临时层。当会话层超预算时从最老的一轮开始丢丢之前先判断这轮里有没有关键信息需要提取到记忆层。这套逻辑写起来不复杂但效果立竿见影我那客服 Agent 的响应时间直接回到了 2 秒以内。注意裁剪一定要在组装上下文之前做而不是等模型报“超出长度”了再补救。后者会导致请求失败重试白白浪费一次调用。3.4 上下文隔离并发场景下的必修课前面提到的状态串台问题根源就是上下文对象没有做隔离。我的做法是每次请求都创建一个全新的上下文实例所有可变状态都挂在这个实例上绝不使用全局变量或类级别的共享状态。如果用了异步框架还要注意协程之间的数据传递确保每个协程拿到的是自己的那份上下文。在 Rust 这类语言里所有权机制天然帮你规避了很多共享问题但如果你用ArcMutex这种共享可变状态一样会踩坑。我的原则是上下文对象在请求生命周期内只被一个执行流持有需要跨阶段传递时用值传递或明确的克隆而不是共享引用。这个原则听起来简单但能挡掉一大半并发相关的诡异 bug。4. 任务调度与并发让 Agent 扛住真实流量4.1 单请求执行流程的拆解要谈并发先得把单个请求的执行流程理清楚。一个典型的 Agent 请求大概经历这几个阶段接收输入、组装上下文、调用模型、解析模型输出、判断是否需要调用工具、执行工具、把工具结果回填上下文、再次调用模型、生成最终回复。这里面模型调用和工具调用都是 IO 密集型的耗时占了大头。理解这个流程很重要因为它决定了并发的粒度。你不能简单地把整个请求当成一个黑盒去并发那样资源利用率很低也不能把粒度切得太细那样调度开销又会吃掉收益。我的经验是以“模型调用轮次”为基本调度单位比较合适每一轮模型调用加上它触发的工具调用作为一个原子单元。4.2 并发模型的选择同步、异步还是队列这三种模型我都用过各有适用场景。同步阻塞最简单一个请求占一个线程适合低并发、逻辑简单的场景但一旦并发上来线程池瞬间打满。异步非阻塞是主流选择用事件循环处理 IO 等待单机就能扛住不错的并发量但代码复杂度高调试也麻烦。队列化是把请求丢进消息队列由固定数量的 worker 消费好处是天然限流、削峰填谷坏处是引入了额外延迟和运维成本。我现在的默认选择是异步为主、队列兜底。正常流量走异步当队列积压超过阈值时自动降级到队列模式保护后端模型服务不被打垮。这个切换逻辑本身也是 Harness 的一部分需要提前设计好。4.3 限流、熔断与降级的实操配置限流我一般做两层入口限流控制总并发数出口限流控制对模型服务的调用速率。入口限流用令牌桶算法桶大小根据机器配置定我一般设成 CPU 核数的 4 到 8 倍。出口限流更关键因为模型服务通常有 QPS 限制超了就直接报错。我会给每个模型供应商单独配一个限流器速率设成官方限制的 80%留点余量。熔断是当模型服务连续失败达到阈值时直接快速失败一段时间避免雪崩。我一般设连续 5 次失败触发熔断熔断 30 秒后半开试探。降级则是熔断期间的备选方案比如切换到更小的模型、返回缓存结果、或者给用户一个友好的等待提示。这三者配合起来Agent 在面对后端抖动时就不会整个挂掉。机制触发条件处理动作恢复策略入口限流并发数超阈值排队或拒绝并发下降自动恢复出口限流调用速率超阈值延迟发送速率下降自动恢复熔断连续失败 5 次快速失败30 秒后半开试探降级熔断触发切换备用方案主服务恢复后切回4.4 任务优先级与公平性真实场景里不是所有请求都一样重要。付费用户的请求、实时交互的请求优先级应该高于批处理任务。我会给每个请求打一个优先级标签调度时高优先级先出队。但要注意防止低优先级任务饿死所以会加一个老化机制任务等待时间越长优先级逐渐提升。这个逻辑不复杂但能显著改善用户体验。5. 工具调用与错误处理Agent 的“手脚”要听话5.1 工具调用的完整生命周期工具调用是 Agent 区别于普通聊天机器人的核心能力但也是最容易出问题的环节。一次完整的工具调用包括模型决定调用哪个工具、生成参数、Harness 校验参数、执行工具、捕获结果或异常、把结果格式化后回填上下文。这里面每一步都可能出错参数格式不对、工具超时、返回结果解析失败任何一个环节没处理好整个任务就断了。我的做法是给工具调用加一层适配器所有工具都通过统一的接口注册和调用。适配器负责参数校验、超时控制、异常捕获和结果标准化。这样业务代码只需要关心工具本身的逻辑不用重复处理这些通用问题。5.2 参数校验别信模型给的参数这一点我要重点强调永远不要相信模型生成的工具参数是合法的。模型可能会给出格式错误的 JSON、超出范围的数值、不存在的枚举值。如果不校验直接传给工具轻则报错重则造成数据损坏。我一般用 schema 校验每个工具定义一份参数 schema调用前先校验不通过就返回错误信息让模型重新生成。这里有个技巧错误信息要写得足够具体告诉模型哪里错了、应该是什么格式这样模型下一轮大概率能改对。如果只返回一句“参数错误”模型往往会原地打转反复生成同样的错误参数。5.3 超时、重试与幂等性工具调用必须设超时这是铁律。我一般给外部 API 类工具设 10 秒超时数据库类设 5 秒本地计算类设 2 秒。超时后是否重试要看工具是否幂等查询类可以重试写入类要谨慎最好带上幂等键。重试次数我一般设 2 次加上指数退避避免瞬间打爆下游。提示重试一定要区分错误类型。网络超时可以重试参数错误重试没意义业务逻辑错误重试可能造成重复操作。我见过有人无脑重试结果给用户发了三封一样的邮件。5.4 错误信息的结构化回传工具执行失败后怎么把错误信息回传给模型直接影响 Agent 能不能自我修复。我的做法是把错误结构化包含错误类型、错误描述、可能的修复建议。比如“参数 user_id 格式错误应为数字实际收到字符串 abc”。模型拿到这种信息往往能自己纠正。如果只是抛一个异常堆栈模型基本无能为力。6. 状态持久化与可观测性出事了能查、重启了能续6.1 会话状态存哪里、怎么存Agent 的会话状态包括对话历史、中间结果、任务进度等。这些状态必须持久化否则服务一重启用户的任务就全丢了。存储选型上我一般用 Redis 存热状态用关系库或对象存储存冷状态。热状态设过期时间比如 24 小时冷状态长期保留用于审计和分析。存储格式上我倾向于用 JSON 序列化可读性好排查问题方便。但要注意版本兼容状态结构变更时要有迁移方案否则老数据反序列化会失败。6.2 全链路追踪的关键埋点可观测性是 Harness 工程的“眼睛”。我会在几个关键位置埋点请求进入、上下文组装完成、每次模型调用前后、每次工具调用前后、请求结束。每个埋点记录时间戳、耗时、token 消耗、状态码。这些数据汇总起来就能画出完整的执行链路出问题时一眼看出卡在哪。trace_id 要贯穿整个链路从入口生成透传到所有下游调用。这样即使日志分散在多个服务也能通过 trace_id 串起来。我踩过的坑是早期没做透传结果排查一个跨服务的问题花了整整一天。6.3 日志分级与敏感信息脱敏日志要分级DEBUG 级别记录详细上下文INFO 级别记录关键节点ERROR 级别记录异常。生产环境默认 INFO需要排查时临时开 DEBUG。但要注意敏感信息脱敏用户输入、工具返回结果里可能包含隐私数据落日志前要过滤。我一般用正则匹配手机号、邮箱、身份证号等模式替换成占位符。6.4 指标监控与告警阈值除了日志还要有指标。我关注的核心指标包括请求量、成功率、P95 延迟、token 消耗速率、工具调用失败率、熔断触发次数。这些指标接入监控系统设好告警阈值。比如成功率低于 95% 告警P95 延迟超过 5 秒告警。告警要能定位到具体环节而不是只报一个“Agent 异常”。7. 常见问题与排查技巧实录7.1 高频问题速查表问题现象可能原因排查方向解决思路响应越来越慢上下文膨胀看每轮 token 数加裁剪和摘要并发下结果串台状态共享查上下文实例请求级隔离工具调用频繁失败参数不合法看校验日志加 schema 校验模型反复重试同一错误错误信息不具体看回传内容结构化错误描述服务重启任务丢失状态未持久化查存储加持久化层后端抖动导致雪崩无限流熔断看调用速率加限流熔断降级7.2 几个我踩过的深坑第一个坑是上下文对象复用。早期为了省内存我把上下文对象做了池化结果并发下互相污染。后来改成每次请求新建内存开销其实没想象中大稳定性提升明显。第二个坑是重试没有区分幂等性。有个写操作的工具被重试了三次导致数据重复。后来给所有写操作加了幂等键问题才解决。第三个坑是日志脱敏不彻底。有一次排查问题把生产日志导出来发现里面全是用户手机号吓出一身冷汗。后来加了统一的脱敏中间件所有日志出口都过一遍。7.3 性能调优的几个实用技巧上下文组装是 CPU 密集型操作如果每次都重新拼接字符串开销不小。我的做法是缓存系统层和记忆层的组装结果只重新拼接会话层和临时层。另外token 计数不要每次都调模型的分词接口本地用轻量分词器估算即可误差在可接受范围内。模型调用的连接复用也很关键。如果每次都新建 HTTP 连接握手开销会吃掉不少性能。用连接池复用实测能降低 20% 左右的延迟。8. 写在最后Harness 工程的取舍之道做 Harness 工程这几年我最大的体会是没有银弹只有取舍。上下文裁剪得越狠token 省得越多但信息丢失的风险也越大重试次数设得越多成功率越高但延迟和重复操作的风险也越大并发度开得越高吞吐越大但稳定性和一致性越难保证。每一个参数背后都是权衡没有标准答案只有适合你当前场景的答案。我的建议是先把核心机制搭起来哪怕参数拍脑袋定也比没有强。然后在真实流量中观察、调整、迭代。Harness 工程不是一次设计出来的是跑出来的。你会在一次次线上问题中慢慢找到属于你自己业务的那套最优配置。最后分享一个小技巧给 Harness 层写一套完整的单元测试和集成测试模拟各种异常场景——模型超时、工具报错、上下文超限、并发冲突。这些测试平时跑着不起眼但每次改 Harness 代码时它们就是你的安全网。我现在的项目里Harness 层的测试覆盖率保持在 85% 以上改代码心里踏实多了。
RELATED READING

延伸阅读

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