一行代码拿到生产级Agent:Strands Harness实战
如果你写过 Agent大概率写过这种循环调一次模型看它返回里有没有 tool_calls有就执行工具把结果拼回消息再调一次模型直到它不再要求调用工具为止。看起来不难可一旦要上生产这个循环马上变得千疮百孔。Strands Agents Harness SDK 就是我在这个系列第 227 篇想认真聊聊的项目——它的核心思路是把“手写 Agent 循环”压缩成“一行代码拿到生产级 Agent”把循环调度、工具调用、上下文管理、重试、并发这些本该由基础设施解决的事情收敛到一个可配置的 Harness 里。这篇文章适合正在做 Agent 应用落地的工程师也适合刚学完 prompt 工程、想往前再走一步的人。1. 手写 Agent 循环的日常痛点从“能跑”到“生产级”之间差了什么1.1 一个“正常”的手写循环长什么样我先给你看一段非常典型的 demo 代码几乎所有从零开始写 Agent 的人都写过类似的版本。messages [{role: user, content: user_input}] while True: response client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolstools, ) msg response.choices[0].message messages.append(msg) if not msg.tool_calls: return msg.content for tool_call in msg.tool_calls: result execute_tool(tool_call.function.name, tool_call.function.arguments) messages.append({ role: tool, tool_call_id: tool_call.id, content: str(result), })这段代码跑一个天气查询、订单查询之类的 demo 完全没问题。你给它一个工具列表它就能在“调模型—执行工具—把结果喂回去”这个循环里转起来。我见过很多演示项目本质都是这个模式的变体只是把execute_tool换成了数据库查询、搜索接口、代码解释器。问题在于这个循环只解决了“能不能跑”完全没解决“能不能稳定跑”。一旦你把它暴露给真实用户各种边界情况会在一小时内把你的日志刷爆。1.2 藏在循环外面的那些“边缘问题”我现在随手就能列出八件那个 while 循环没处理的事情上下文爆掉。Agent 每执行一轮工具就要把工具返回塞进 messages来回十几轮之后输入长度很容易超过模型窗口上限。你没做截断模型直接报错。工具返回内容过大。有些工具会把一整个 SQL 查询结果或整个文件内容返回给模型一次就吃掉几万个 token你却毫无感知。参数解析失败。模型返回的arguments是字符串理论上应该是 JSON但真实世界里它偶尔会多一个反引号、少一个右括号json.loads直接抛异常。工具执行异常。工具抛了个RuntimeError这个错误信息如果没有作为 tool 消息喂回给模型Agent 就卡在原地用户只看到转圈。循环死循环。模型反复要求调用同一个工具永远不给最终答案你的 while 循环只能靠进程重启来终止。重试策略缺失。模型服务限流、网络抖动、超时都需要重试但手写循环里通常只在外面包一个大 try失败整个 Agent 就中断了。并发控制为零。十个用户同时触发 Agent每个循环都独立占用资源数据库连接和模型 API 并发瞬间被打满。可观测性没有。你根本不知道 Agent 正在做什么、上一轮模型调用了哪个工具、为什么在这个分支上循环了三次。这些不是“将来可能需要优化”的细节而是“生产级”和“demo 级”的分水岭。你说的“Agent 执行终止于错误”底层原因大多是其中某一条没兜住。手写循环不是难而是琐碎琐碎到每一个坑单独看都不大但它们叠在一起足以让你在项目后期花大量时间去补。2. Strands Agents Harness SDK 的定位与设计思路2.1 Harness 和 Agent 到底有什么区别很多人第一次看到“Harness”这个词会懵它和 Agent 是什么关系我用一个类比来解释Agent 是坐在驾驶位上的司机负责判断路线、决定要不要打方向盘Harness 是整辆车和仪表盘负责把司机的每个动作安全地转化成执行结果。如果一个 Agent 只有“智能”没有 Harness那就像司机坐在一堆裸露的电线上开车思想再清晰也没用。Strands 这个项目切入的点恰恰是大多数框架容易忽略的部分——它不替你选模型、不绑架你的 prompt 风格而是把你写循环时不得不做的事变成一个可以配置、可以扩展的运行时。GitHub 上这个仓库给我的第一印象是它终于把 Agent 工程化里的“过程控制”当成了头等公民。所以讨论“harness 和 agent 区别”时我的理解是Agent 定义的是“这个智能体会做什么”Harness 定义的是“这个智能体如何稳定地把事做完”。一个偏决策一个偏执行。Strands 的agent对象里可能只写了模型和工具清单真正让 agent 跑起来的循环、重试、截断、日志全部由 harness 接管。2.2 核心抽象Agent、Tool、Runtime、Policy从代码风格来看Strands 的设计非常接近我喜欢的模式核心概念少组合关系清晰。大致可以归纳为四个东西Agent一个业务单元负责持有模型配置、系统提示词、工具清单、运行策略。你定义一个 Agent就像定义一份“岗位说明书”。Tool一个可被模型调用的函数及其 Schema。Strands 里注册工具非常自然装饰器一挂函数签名自动转换成工具的 JSON Schema。RuntimeHarness执行循环的调度器。它接收用户输入然后驱动 Agent 和 Tool 一轮一轮地交互直至结束。Policy在 Runtime 各个节点注入的“规则”。比如最多跑多少轮、上下文超过多少 token 触发压缩、工具执行超时多久、失败重试几次。这套抽象的好处是你在写 Agent 的时候不再往业务代码里混入“循环控制”和“错误处理”的脏活。比如你想让 Agent 最多转 15 轮不需要在 while 循环里数数而是在配置里写一个字段。你想给每个工具执行加 30 秒超时同样改配置即可。更难得的是策略是显式的。手写循环里很多决策隐藏在你的if和try里别人没法改Strands 把决策变成了配置项和钩子团队里所有人都能看懂 Agent 的“行为边界”。这对做工程的人来说太重要了。2.3 “一行代码”不是魔法而是默认策略帮你在扛标题里说“一行代码拿到生产级 Agent”这句话很容易被误解成“什么配置都不需要写”。我的理解是它把生产环境里常用的一整套默认值打包了。你写await harness.run(agent, user_input)这句代码背后默认帮你做了几件事自动维护 messages 序列、自动检测 tool_calls 并执行、自动把工具执行结果拼回上下文、设置了一个合理的最大迭代上限、给模型调用加了超时和重试、输出结构化的事件日志。这些默认策略不是拍脑袋定的。它们来自真实 Agent 应用最容易出错的地方。比如默认的最大迭代次数通常不会设成无限而是 10 到 20 轮既能覆盖多数复杂任务又能防止模型陷入死循环。再比如工具执行结果默认会被截断到一定长度避免一次工具返回撑爆上下文。这些就是“生产级”的定义你不需要写代码但它已经按经验值做了最保守、最稳妥的处理。你当然可以覆盖这些默认值。但关键是新手拿来就能得到一个不会轻易“转圈转死”的 Agent老手则可以顺着钩子把策略替换成自己的实现。这种“默认好用、开放可改”的平衡是很多框架没有做到的。3. 用最小例子拆解“一行代码”背后发生的事3.1 安装与初始化按照仓库 README安装很简单常规 Python 项目一条 pip 命令就能搞定pip install strands-agents-harness如果你喜欢直接从源码跑最新版本也可以 clone 仓库之后pip install -e .。我个人建议第一次用直接装 PyPI 版本先把流程跑通再考虑源码。初始化时只需要把模型访问密钥配好。Strands 本身没有绑定某一家模型厂商只要底层兼容 OpenAI 风格的接口就可以直接通过配置切换。项目里一般用一个.env文件管理密钥加载方式和你惯用的python-dotenv一致。3.2 定义工具和 Agent 配置下面我以一个极简的“订单查询助手”为例。先定义一个工具函数from strands_agents import tool tool def get_order_status(order_id: str) - str: 查询订单当前状态 status_map {A1001: 已发货, A1002: 已签收} return status_map.get(order_id, 订单不存在)装饰器会把函数名、docstring、参数类型自动映射成模型可识别的工具 Schema不需要手工维护 JSON。然后把 Agent 写在一个配置文件里这比把配置写在代码里更适合团队协作agent: model: gpt-4o-mini system_prompt: 你是订单助手只能基于工具结果回答用户问题。 tools: - get_order_status policy: max_iterations: 10 timeout_seconds: 30 retry: 3最好再准备一个入口脚本import asyncio from strands_agents import Agent, Harness async def main(): agent Agent.from_config(agent.yaml) harness Harness() result await harness.run(agent, A1001 这个订单现在到哪了) print(result) asyncio.run(main())就这么简单。你的业务代码里不再有 while 循环不再有messages.append不再有json.loads和异常处理。那一段“很脏”的逻辑全部被 Harness 收编了。3.3 执行流程从用户输入到最终回答虽然表面只有一行runStrands 在内部做的事情其实还是我们手写循环的那套流程只不过每个节点都加了控制和观测。大致是把用户输入和系统提示词、历史消息打包成初始 messages。调用模型得到这次回复。如果回复里有tool_calls按顺序校验工具是否存在、参数是否合法。执行对应工具函数并给工具执行设置超时和错误捕获。把工具执行结果以tool角色消息追加到上下文中。重复步骤 2 到 5直到模型不再请求调用工具或迭代次数达到上限。返回最终文本同时把整个执行轨迹存下来。看到没这个流程和我第一节写的手写循环是同一个骨架。区别在于每个节点都被加固了第 2 步失败会重试第 3 步参数解析失败会尝试修复第 4 步工具抛异常会把错误信息作为消息反馈给模型第 6 步有 max_iterations 兜底。这给了我一个很大启发Agent 应用的架构并不神秘本质还是一个“模型调度工具”的循环。真正拉开差距的是你在循环的每一圈上做了多少防御。3.4 值得细看的高级选项用 Strands 一段时间后我建议你把目光从“能跑”转向这几个配置项上下文压缩策略。默认超过阈值时Harness 会丢弃最早的部分消息或做摘要。生产环境里这是保命设置。工具并发度。如果你的业务允许同时执行多个工具调用Strands 的运行时可以并行执行一个回合里的多个tool_calls并限制并发上限。回调钩子。你可以在“模型调用前”“工具执行后”“Agent 结束”等节点挂上自己的函数把事件推送到日志、监控、或追踪系统。沙盒模式。针对不可信工具输入可以启用受限执行环境避免工具函数直接操作系统资源。这些选项刚开始不需要全部打开但知道它们存在很重要。因为等你的 Agent 真的遇到生产事故时你会发现你不必临时改架构只要在配置里打开对应的开关或者在钩子里补一段逻辑就行。4. 从手写循环迁移到 Harness一张对比表和迁移路线4.1 九个维度的对比我把自己过去手写循环和改用 Strands 之后的体验整理成了表格可能对你做技术选型有帮助维度手写 Agent 循环Strands Agents Harness循环终止控制自己维护 while 和 break容易漏配置 max_iterations自动兜底死循环上下文管理手动 len(messages) 截断容易误删内置截断/摘要策略可自定义工具注册手动维护 tools 列表与函数映射装饰器生成 Schema自动映射参数解析json.loads 后看运气Schema 校验 错误反馈给模型工具异常处理只知道程序崩了捕获异常后作为消息回传Agent 可自救重试机制手写 except 重试容易重复请求统一重试策略支持退避并发控制基本没有内置信号量可配置最大并发可观测性print() 大法结构化事件回调方便接监控执行超时容易忽略Policy 统一设置超时并强制中断这张表不是在说“手写方案一无是处”。对于一次固定的、不需要工具调用的问答手写循环反而是多余动作。但只要你的 Agent 需要调用多个工具、需要跟用户多轮交互Harness 的优势就很明显了。4.2 迁移时最容易犯的三个错误我从自己的踩坑经历里总结出三个高频错误希望你能绕开第一个错误把所有任务塞进一个大工具。很多人把“查订单”和“计算价格”写在一个函数里然后靠参数分支。这会让模型的工具选择变得困难也违背了工具设计的基本逻辑。迁移到 Strands 时应当把函数拆细每个工具只做一件事命名和描述足够清晰。第二个错误忽略工具 Schema 的精确性。工具函数的参数用str或object会让模型很迷茫。你要尽量用精确的类型和描述比如order_id: str后面注明“订单号例如 A1001”。Schema 越清晰模型调用工具的准确率越高。第三个错误一上来就改默认策略。Harness 的默认值已经经过实践打磨直接跑大概率没问题。有些人恨不得第一天就把 max_iterations 改成 100、关掉截断结果上下文爆掉后又回来骂框架。我建议先保留默认值跑几天真实流量再根据日志调整。4.3 渐进式迁移路径如果你有一个已经跑起来的手写 Agent不用推倒重来。我推荐按下面五步渐进迁移第一步把现有工具函数全部用tool装饰器包起来先让结构和 Strands 一致。第二步写一个agent.yaml把模型、系统提示词、工具清单都放进去业务逻辑不改。第三步用harness.run替换原来的 while 循环其他代码暂时保留观察输出是否一致。第四步在 harness 的回调里接入日志和监控把之前的 print 逐步删掉。第五步加入并发请求测试验证策略配置是否达到预期。这套路径的好处是每一步都能独立验证风险很小。你不需要在一个周末里重写全部代码。5. 实践中翻车的三个场景与修正5.1 场景一Agent 执行中途报“execution terminated due to error”这个报错第一次出现时很吓人感觉 Agent 突然死掉了。其实它通常只是触发了 Harness 的运行时兜底策略。常见的诱因有三个一是工具执行抛了未捕获异常。二是某个模型调用延迟超过timeout_seconds被强制中断。三是连续多次执行失败重试用尽Harness 直接终止。排查思路不要从报错本身开始而是去看结构化事件日志。Strands 会在每个节点发事件比如on_tool_failed、on_model_timeout、on_agent_terminated。你登录后台看最后几个事件基本就能定位是哪一环出了问题。修正也简单如果是工具异常去修工具的边界处理如果是超时把对应接口的阈值调大如果是重试耗尽检查是不是模型频频返回格式错误。这个报错其实是个好事。手写循环里很多错误是被静默吞掉的最后给你一个牛头不对马嘴的结果而 Harness 至少会把 Agent 的执行过程显式地结束告诉你“这里出了问题”。生产环境里显式的失败远远好过隐性的错误。5.2 场景二模型返回了不合法 JSON 参数模型输出工具参数时偶尔会出现典型的“幻觉 JSON”多了前缀说明、truncated 了一段、或者把单引号当双引号用。手写循环里这一下就会让整个 Agent 崩溃。Strands 的做法比较稳妥它会先按 Schema 做解析如果失败会把解析错误信息作为一条 tool 消息反馈给模型让它修正参数后再次调用。这么做的好处是不打断循环模型看到“你上次调用 get_order_status 时参数不是合法 JSONxxx”之后往往下一轮就能给出正确格式。如果你遇到这个错误频繁出现说明你的工具 Schema 描述可能不够清楚或者模型温度设置偏高。我一般会把工具参数描述写得像给实习生看一样“order_id必填字符串来自用户原始输入的订单编号不要自己编造。”这个小小的改动能把参数解析失败率降一个数量级。5.3 场景三并发一高模型 API 限流上下文越来越长接入真实用户后你迟早会遇到并发问题。几十个用户同时触发 Agent模型 API 开始返回 429或者同一个用户反复追问上下文越来越长模型响应变慢、费用飙升。针对限流Strands 这类的 Harness 都会内置并发限制你可以设置最大同时运行的 Agent 数量其余请求进入队列等待。针对上下文膨胀可以开启自动摘要或截断策略。我的建议是双管齐下控制并发同时限制单轮上下文长度的上限。另外要特别提醒一点不要把所有人的所有历史消息都无限堆在一个 Agent 会话里。如果用户的问题与历史无关开启新会话的成本反而更低。很多“上下文太长导致出错”的案例根因不是框架问题而是产品层面的会话设计问题。6. 进阶玩法多 Agent 协作与自定义策略6.1 从单 Agent 到多 Agent 编排等单个 Agent 稳定运行后你大概率会开始琢磨多 Agent 协作。Strands 的抽象让它也可以充当简单的编排层你可以创建多个 Agent各自负责不同领域由一个主 Agent 来分发任务。比如订单助手负责查单售后助手负责退款主 Agent 根据用户意图选择把问题交给谁。常见的做法是让主 Agent 的工具列表里包含其他 Agent 的run入口。这样对主模型来说调用一个子 Agent 就像调用一个工具描述清楚、输入输出可控。多 Agent 的粒度不要一开始铺太大三个以内最好否则你会被协调成本淹没。我在实际项目里的体会是多 Agent 的收益更多来自“隔离”。不同领域用不同模型、不同 prompt、不同工具预算能有效防止一个 Agent 的上下文被无关操作污染。Harness 提供了这种隔离的边界。6.2 自定义 Hook 与策略扩展如果你需要更精细的控制可以挂自定义钩子。比如我想在每次工具调用前打印参数在每次模型调用后发送指标到 Prometheus只需要实现几个回调from strands_agents import Harness class MonitorHarness(Harness): async def on_tool_start(self, ctx, tool_call): log.info(tool%s args%s, tool_call.name, tool_call.arguments) async def on_model_response(self, ctx, response): metrics.inc(model_calls, 1) metrics.observe(latency_ms, response.latency_ms)然后把这个MonitorHarness替换默认实例就行。业务代码完全不动观测能力就接上了。这种“可替换 Harness”的设计让项目不会因为框架的默认能力不够而卡住。6.3 什么情况下不建议用这个 SDK写这篇文章不是为了无脑吹。我同样会建议一些场景别用它如果你的“Agent”只是固定顺序调用两三个 API没有模型自主决策那么直接写函数更干脆。如果你对循环的每一步都要做极其特殊的定制框架的抽象反而会碍手碍脚手写循环更合适。如果你的模型供应商接口不是 OpenAI 风格、也没有适配插件可能需要自己写适配层那就要权衡成本。框架的价值在于覆盖 80% 的通用场景。剩下 20% 的特殊需求需要你自己评估是不是值得引入一个 Harness。我个人判断标准很简单只要我的 Agent 会调用两个以上工具、并且需要上生产我就会优先考虑 Strands 而不是再手写一个循环。7. 最后说几句实操感触用了 Strands 一段时间后我最深的感受不是“代码少写了几百行”而是错误处理和可观测性终于有了统一出口。以前排查 Agent 问题是在各种 print 之间猜现在是看事件日志就能定位到具体是模型调用超时还是工具返回异常。如果你正在手写循环里挣扎我建议先拿一个真实业务工具做迁移试点不要全量改造跑通后再逐步扩大范围。等你看到 Harness 在并发、重试、上下文管理上替你扛住那些琐碎问题之后应该就能理解为什么现在做 Agent 应用大家越来越强调“运行时”而不是“套 Prompt”了。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →