尧图精选

从手写Agent循环到一行代码:生产级Harness的工程实践

🕒 发布时间:2026/10/2 20:11:51 📁 来源:尧图网络
先给大家交个底标题里这句“从手写 Agent 循环到一行代码拿到生产级 Agent”我第一眼看的时候是持怀疑态度的。毕竟这两年 Agent 框架多如牛毛十个里有八个都是套壳 Chat真正能解决“循环失控、上下文爆炸、并发翻车”这三个老问题的没几个。直到我实际把 Strands Agents Harness SDK 拉下来跑了一轮又把一个线上小项目从手写循环迁了过去才确定这个项目有值得聊透的价值。这篇不是软文就是一次完整的拆解和实操记录。我会先讲清楚为什么手写 Agent 循环听着简单、做着全是坑再拆 Strands Agents Harness SDK 的核心设计最后把“一行代码”背后的参数、并发模型、错误处理、可观测性这些容易翻车的地方全部摊开讲。不管你是刚接触 Agent 开发还是已经在维护一套带工具调用的业务系统这篇都能让你少踩几个坑。1. 手写 Agent 循环看着几行代码背上全是债1.1 一个最朴素的 Agent 循环到底长什么样很多人的 Agent 之路起点都是同一个东西一个 while 循环。把用户问题塞进 messages调一次模型如果模型返回了 tool_calls就执行工具、把结果追加回上下文再继续调模型直到模型给出最终回答。写成伪代码大概是这样while not done: response llm.chat(messages) if response.has_tool_calls(): for call in response.tool_calls: result dispatch_tool(call.name, call.args) messages.append(tool_result(call.id, result)) else: done True说实话这个骨架在 demo 里跑通是真的快我当时第一个原型也是这么写的。但问题在于这个循环只是“看起来简单”它把一整套工程问题全部隐藏在了省略号里。一旦你把它从 Jupyter Notebook 挪到生产环境接上真实用户、真实工具、真实流量省略号里的内容就会变成一座冰山。不是说要否定手写循环而是得承认循环本身只是 Agent 的骨架骨架之外的状态管理、上下文窗口、超时控制、并发隔离才是生产环境里真正吃时间的地方。大多数手写实现到最后都长成了一个谁也改不动的大泥球。1.2 从“能跑”到“能上线”循环里到底藏了哪些坑我把实际踩过的坑归一下类基本就是四类。第一类循环停不下来。模型偶尔会连续输出 tool_calls 不给出最终答案或者两个工具互相调用形成死循环式的自洽。你如果不设置 max_iterations一次请求能烧掉你几万 token 的账单。我见过最多的一次是某个内部测试里模型连续调了 37 次工具最后返回了一个和最初问题完全无关的结论。第二类上下文爆炸。每一轮工具结果都往 messages 里塞肉眼可见地让 prompt 越来越长。模型输入长度一上去延迟随线性上升成本随线性上升准确率反而下降。你以为是模型变笨了其实是你的上下文塞了太多历史垃圾。第三类错误恢复基本靠试错。工具接口不可能永远稳定第三方 API 超时、参数校验失败、偶发 500这些在单次调用里可以重试但在一个多轮循环里任何一个工具失败都可能污染整个会话状态。手写方案往往就是 try-except 一把梭重试策略、降级策略、止损策略统统没有。第四类并发完全失控。Agent 循环是有状态的每个用户一个会话每个会话一堆中间状态。你用全局字典存内存爆你用数据库存连接池爆你直接在线程里开 goroutine 或线程池LLM 的 API 并发额度先爆。到这一步你就明白了手写的不是 Agent是定时炸弹。所以“Harness”这个词其实点得很准Agent 解决的是“决策”Harness 解决的是“决策之外的所有事”。这也是很多人问“harness 和 agent 区别是什么”的本质答案——Agent 是大脑Harness 是身体、循环、代谢和免疫系统。2. Strands Agents Harness SDK 的核心设计思路拆解2.1 Agent 与 Harness 的分层把“决策”和“执行”彻底分开Strands Agents Harness SDK 第一个让我觉得舒服的设计就是它没有重新发明 Agent 的概念而是把 Agent 定义得非常薄Agent 就是一个模型、一段系统提示词、一组工具声明。它只负责在给定上下文时做出“下一步动作”的决策可能是输出文本可能是请求调用某个工具。至于这个决策怎么被循环执行、怎么管理状态、怎么处理上下文窗口、怎么重试失败一概不管。Harness 则是承接所有脏活累活的那一层。它负责运行循环、调度工具、管理会话状态、截断历史、控制并发、记录日志。如果你写过 Web 框架会发现这个分层思路和“业务代码与框架代码分离”是同一个逻辑只不过这次业务代码是 Agent 的决策逻辑框架代码是 Harness。这种分层带来的直接好处是你可以随意换模型、换工具、换提示词而 Harness 的逻辑完全不用动反过来也一样你想把运行策略从“顺序执行”改成“并行探索”或者给每个请求加一个全局超时也只动 Harness 配置。以前手写循环的时候这些东西是耦合死的换一个模型等于重写一遍调度逻辑。另外一个很实用的点是Agent 层保持无状态。无状态意味着你可以在并发场景下用同一个 Agent 定义服务成千上万个会话只要会话状态由 Harness 统一管理即可。这一点在后面的并发章节会详细讲先记住这个结论。2.2 状态管理与上下文窗口控制每个会话的“记忆”到底存哪Agent 循环的无状态不代表整个系统可以无状态。恰恰相反整个系统里最核心的就是状态。Strands Agents Harness SDK 把状态分成了两层会话状态和循环状态。会话状态是面向业务层面的比如用户是谁、当前订单 ID、已经收集了哪些信息、工具调用产生的重要结果。这部分会持久化到你在 harness 配置里指定的存储后端可以是内存、Redis 或者数据库。循环状态则是每次运行内部的临时变量比如已经执行了多少轮、每一步产生的中间输出、当前上下文截断到了哪里这部分只在一次 run 的生存周期内有效。这个分离我一开始没太当回事直到我处理一个“用户中途打断重新问”的场景才发现它多有用。手写方案里上下文已经被工具结果塞得乱七八糟用户换个问法整个对话线索就断了。而 Harness 把关键业务状态独立抽取出来之后即使历史消息被截断核心状态依然还在Agent 不会“失忆”。上下文窗口控制也是 Harness 的重点。它默认会根据模型的 max context 自动计算可用窗口并采用两种策略截断和摘要。截断就是只保留最近 N 轮消息把最老的消息直接丢掉摘要是把超长的工具结果交给一个小模型或者用规则做压缩保留关键信息再塞回上下文。你甚至可以用 summarize threshold 调节这个阈值比如当总 token 数超过整个窗口的 60% 时触发摘要。2.3 工具注册与安全边界模型不能什么都直接调工具调用是 Agent 能力的放大器同时也是事故高发地。Strands Agents Harness SDK 在工具注册上做了一套校验机制每个工具必须有完整的 JSON Schema 声明Harness 会在调用前校验模型生成的参数调用后校验返回值。这个设计虽然增加了一点注册成本但能有效避免“模型编造参数”这种常见事故。更值得聊的是它对工具权限的分级。你可以给每个工具打上权限标签比如 read-only、user-confirmed、admin-only。read-only 的工具可以自动执行user-confirmed 的工具必须经过用户确认按钮或文案确认后才能执行admin-only 的工具则只能由管理员触发。这个机制直接回应了“agent 安全”这个热门话题尤其是当 Agent 接入了发送邮件、修改订单、调用内部 API 这类高影响操作时没有权限边界就是在裸奔。我在迁移老项目时把原来手写循环里“所有工具一律直接执行”的逻辑改成了三级权限生产事故瞬间少了很多。最典型的一个案例是模型在某个非预期场景下尝试调用退款接口虽然最后 Harmess 拦住了但如果当时没有权限分级后果就很麻烦。安全不是靠模型自觉而是靠框架兜底。3. 从“手写”到“一行代码”完整实操过程记录3.1 一个最小可运行示例直接抄作业先给一个最简示例把整个上手路径走通。假设我们需要一个能查天气、算日期差值的 Agent。from strands_agents import Agent, create_harness from strands_agents.tools import tool tool(nameget_weather, description查询指定城市的实时天气) def get_weather(city: str) - str: # 这里对接你的天气 API return f{city} 当前天气晴25 摄氏度 tool(namedays_between, description计算两个日期之间相差的天数) def days_between(start: str, end: str) - int: from datetime import datetime d1 datetime.strptime(start, %Y-%m-%d) d2 datetime.strptime(end, %Y-%m-%d) return abs((d2 - d1).days) agent Agent( modelqwen-plus, # 对接的模型 system_prompt你是一个生活助手善于使用工具回答问题。, tools[get_weather, days_between], ) harness create_harness( agentagent, max_iterations10, timeout60, history_window20, # 最近 20 轮消息 summarize_threshold0.6, # 上下文超过 60% 时触发摘要 ) result harness.run(北京今天适合户外跑步吗顺便算一下从今天到 2026-01-01 还有几天。) print(result.final_answer)这里最关键的一行就是harness.run(...)。你不需要自己去写 while 循环不需要自己处理 tool_calls 的分发不需要自己拼 messagesHarness 会基于 Agent 的工具声明和系统提示词自动完成整个多轮决策过程。第一次跑通的时候我甚至有点恍惚以前写 200 行循环才能实现的事现在真的就一行。3.2 “一行代码”不等于“无配置”关键参数怎么选虽然入口是一行代码但生产级输出的差异全在参数里。这些参数我建议大家按表里的思路来定不要盲目抄默认值。参数建议值区间说明与选型逻辑max_iterations10~30任务越复杂需要的轮数越多但超过 30 基本意味着循环失控建议结合成本上限设硬顶timeout30~120 秒每次 run 的总超时包含所有内部多轮调用面向用户交互建议 60 秒内异步任务可放宽history_window10~30 轮太大则上下文膨胀太小则 Agent 丢掉早期关键信息有长对话场景时配合摘要策略更好summarize_threshold0.5~0.7触发上下文摘要的阈值60% 是我实测的性价比平衡点concurrency取决于模型 API 配额建议先按预估 QPS 和单轮耗时反推不要盲目拉高retry_policy2 次重试 指数退避对偶发 5xx、限流有效对 4xx 类错误不要重试直接上报这里要特别提醒一下如果你在 async 环境跑记得用harness.arun(...)而不是run(...)这会直接返回一个可等待的 coroutine避免阻塞事件循环。我当时在这个细节上卡了半天明显是没读文档的下场。3.3 从老项目迁移到 Harness一次真实的代码改造记录我的老项目是一个客服工单分流系统原本的架构就是标准手写循环每次用户发消息重启一个循环去调用“意图识别工单查询知识库检索”三个工具。代码量倒不算大但每次加工具都会陷入改循环逻辑的泥潭。迁移步骤我整理成了四步。第一步把工具全部用装饰器重新声明一遍补上 JSON Schema 和权限标签这个工作量最小半天就完了。第二步把原来循环里散落的上下文处理逻辑删掉改成统一由 Harness 的 history_window 和 summarize 管理这一步需要做一次回归测试确保同样的问题在迁移前后回答质量没有明显下降。第三步把会话状态从原来的全局字典迁到 harness 支持的存储后端用会议 ID 做 key。第四步替换入口代码原来几十行的手写逻辑变成harness.run(...)加一个结果包装。整个迁移我用了一个工作日加一个上午痛苦点集中在第二步的一致性验证。我的建议是准备一组固定的测试问题集至少 20 条覆盖正常查询、多工具组合、工具失败、用户中途改口这几类场景迁移前后各跑一遍对比最终答案是否一致。自动化测试可以以后补但第一次迁移一定要人工过一遍。4. 生产级意味着什么并发、可靠、可观测三块硬骨头4.1 Agent 怎么扛并发请求池、背压与 QPS 预估“AI Agent 怎么扛并发”是高频问题也是 Strands Agents Harness SDK 里我比较喜欢的一块。手写循环天然是单会话单线程的并发靠开线程线程多了就炸。Harness 的思路则是把 Agent 执行过程包装成一个个任务丢进一个可控的并发池里由它来统一调度。你可以简单理解成食堂打饭手写方案是一百个人同时挤一个窗口Harness 是排号叫号窗口数workers和排队区queue都有上限。多出来的请求要么排队要么直接拒绝并提示稍后重试这就是背压。配置上我建议按这个公式估算 workers 初始值单轮平均耗时约为 2 秒单会话平均轮数约为 4那么单个请求总耗时约 8 秒。如果模型 API 允许 16 并发workers 设在 16 左右时理论上每秒能服务 2 个新请求如果业务上要求更高的吞吐就得优先提升模型供应商的并发配额而不是无脑调大 workers。我这里专门提一句漫无目的地调大 concurrency 是最常见的翻车姿势。并发池是压到了模型 API 的限流上限返回一堆 429反而把整体吞吐拖垮。Harness 里可以设置 retry_policy 对 429 做指数退避重试但退避只会让请求排队时间变长治标不治本。正确的做法是先压测找出模型 API 的实际吞吐极限再回头调 harness 的并发参数。4.2 错误处理不再被 “agent execution terminated due to error” 支配很多人在日志里见过类似 “agent execution terminated due to error” 之类的报错。这类错误在 Strands Agents Harness SDK 里被明确分成了三种处理路径。第一类是模型侧错误比如超时、API 返回 5xx、上下文溢出。这一类 Harness 会根据 retry_policy 做有限次重试重试次数超过上限后会触发降级策略例如改用一个更小的模型重新生成或者直接返回一个“服务暂时不可用”的兜底回复。第二类是工具侧错误比如工具抛异常、返回格式不符、调用被权限拦截。Harness 会把这一轮的工具执行标记为失败并把错误信息组织成一条 tool_result 返回给模型让模型自行决定是更换参数重试、换一个工具还是放弃。这个设计我特别认可它把错误当作一种观察结果而非终态Agent 的决策连续性不会被轻易打断。第三类是全局性错误比如总超时、迭代上限、上下文异常。这类错误 Harness 会直接终止整个 run 并返回可读的错误码和部分结果方便上层业务处理。我把之前线上的工具调用失败处理从 try-except 改成了利用 Harness 的错误信息返回机制模型在工具出错时的表现好了很多至少不会再对着用户说“系统出错了”而是会主动调整策略继续完成任务。4.3 可观测性日志、Trace 与成本核算一个都不能少生产级框架光能跑是不够的你还得看得懂。Strands Agents Harness SDK 内置了链路跟踪能力每次run会生成一个 trace 树树的根节点是一次完整的请求子节点是每一轮的模型调用、每一个工具的执行、每一次上下文摘要每个节点都记录了耗时、输入输出摘要和 token 用量。我实际调试 Agent 时的习惯是开启 debug 模式打印详细 trace定位多轮调用里的异常轮次。定位思路是这样的先看工具调用的参数是否合理再看工具返回是否被正确追加最后看模型基于这个返回做出的下一步决策。整个链路打出来之后问题往往一眼就能看出来比如模型在第三轮突然传了一个不符合 schema 的参数或者工具结果过长导致上下文被截断。成本核算这块我也要提一句trace 里带 token 数非常关键。我上线后每周会跑一个脚本按 trace ID 聚合 token 消费找出消费最高的几个会话。这些高消耗会话往往是上下文策略没生效或 Agent 陷入无效循环的重灾区一旦发现就回去调 summarize_threshold 和 max_iterations。没有这类数据你优化性能就只能靠猜。5. 常见问题与排查实践我把该踩的坑都替你踩了一遍5.1 问题速查表现象、原因、排查手段整理一个速查表基本涵盖我在这套 SDK 上线前后遇到的高频疑难场景。现象可能原因排查手段Agent 循环停不下来token 消耗异常模型持续输出 tool_calls 不收敛检查 max_iterations 是否设了硬顶查看 trace 中工具返回是否清晰必要时在工具返回里加“无更多信息”的终止信号回答质量随对话轮数增加而下降上下文膨胀导致关键信息丢失检查 summarize_threshold 和 history_window打开 debug trace 看摘要触发时截掉了哪些内容并发上来之后偶发 429 或超时请求池参数压到了模型 API 配额上限先压测找模型 API 同时刻最大并发再回头调整 harness 的 workers不要盲目加大并发池工具被模型在非预期场景调用工具描述或 schema 不精准权限分级缺位重新写工具描述加入触发条件和禁区描述给敏感性工具设置 user-confirmed 权限多次运行结果不稳定模型采样随机性大或提示词引导不足固定 seed如果模型支持或降低 temperature优化 system prompt 加入输出格式约束老会话重新开始时上下文全丢会话存储未配置或 key 使用不对检查会话状态存储后端和会话 ID 注入逻辑确认每次 run 用的是同一个 session_id工具返回过长上下文溢出工具结果未做摘要或截断给高数据量的工具增加返回结果精简逻辑或调低 summarize_threshold这个表看起来简单但每一条我都是实际踩过一轮才总结出来的。尤其是第一条如果不给 max_iterations 设硬顶再便宜的模型也能在某个失控会话里烧出一个让你肉疼的账单。5.2 几个独家调试技巧常规文档里真没有第一个技巧利用 trace ID 串全链路。把每次run返回的 trace_id 记录到业务日志里用户反馈问题时拿着 trace_id 能直接定位到当时的每一步决策和工具调用明细这比让用户截图聊天记录管用得多。第二个技巧刻意制造工具失败来检查 Agent 的鲁棒性。我在测试环境把某个工具故意改成抛异常观察模型在收到错误反馈后是继续尝试、换工具还是直接向用户道歉。如果模型连续三次都在同一个失败工具上打转那说明要么工具描述有误导要么 system prompt 对“如何应对工具失败”的指导不足。第三个技巧对上下文摘要结果做抽样检查。摘要逻辑是自动触发的它可能把关键时间点或数字压丢。我一般是每周抽几个长会话的 trace检查摘要前后模型对关键信息比如订单号、日期、金额的引用是否仍然准确。如果不准就给摘要策略加保护规则把某些字段标记为“不可压缩”。最后再说一点个人体会把 Strands Agents Harness SDK 从“一行代码”真正变成生产可靠系统之后我最大的感受是这类工具的价值不在于省掉那 200 行循环代码而在于它逼着你把 Agent 开发里的模糊地带全部显性化——迭代上限是多少超时怎么处理工具失败如何反馈上下文怎么管理并发压到多少。这些问题的答案以前分散在每个工程师的脑子里现在变成了 Harness 的配置项和 trace 日志。如果你现在还在手写 Agent 循环我的建议是别急着把代码全部推翻重来。先拿一个小项目或者一个非核心流程做迁移实验用 trace 对比迁移前后的决策质量和稳定性。等你真正感受到“改配置比改代码”更高效的时候再逐步铺开。Agent 的技术栈还在快速演进但“决策与执行分离、状态与并发可控、全程可观测”这几个底层原则短期内不会过时。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →