如果你写过几个带工具调用的 Agent,大概率经历过这样的阶段:一开始觉得写个 Agent 循环无非就是while True,让模型选工具、跑工具、把结果再塞回去,十行代码搞定。等真正丢到生产环境,才发现这个循环像一张到处漏风的网——模型偶发返回一段非法 JSON、工具超时没兜底、上下文越滚越满、账单也在悄悄往上飙。今天这个开源项目Strands Agents Harness SDK,就是把这层脆弱的“手写 while”换成一个有明确边界、有重试策略、有上下文治理、有链路追踪的工程化装配层。它不是又一个 Agent 框架,而是一个“接管循环”的 SDK:你只需要提供你自己的业务逻辑和工具函数,它用一行代码帮你拿到生产级 Agent。
在继续往下说之前,先给读者一个定位。这个项目适合谁?适合已经用 LangChain、自研 Agent 或裸调 LLM 写过 agent、但正在为“怎么稳定上线”头疼的开发者。如果你刚刚接触 LLM 应用开发,手里的项目还在原型阶段,同样可以读下去,因为里面聊的“循环失控、上下文溢出、重试抖动”这些问题,你早踩晚踩都会踩到。今天这篇不打算讲泛泛的概念,直接拆实现、讲策略、给代码、踩坑实录都放在后面。
1. 先从“手写 Agent 循环”这件苦差事说起
1.1 一个看似简单的循环
大部分 Agent 初版代码长这样:
while True: response = llm.chat(messages, tools=TOOLS) if not response.tool_calls: break for call in response.tool_calls: result = EXECUTOR.run(call.name, call.arguments) messages.append({"role": "tool", "name": call.name, "content": result})单看这个循环,逻辑是顺的:模型说要调工具,就去调,调完把结果追加回消息列表,再让模型判断是否还有下一步。这也是网上教程最多的一种写法,跑通 Demo 没有任何问题。问题是生产环境不会按 Demo 的逻辑出牌。
1.2 生产环境会把你拉回现实
我自己的项目里遇到过这么一串事故,几乎每一项都对应上面那几行代码的一个盲区:
- 工具函数抛出异常,整个循环直接崩溃,用户侧看到的是 500。
- 模型一次返回 5 个工具调用,循环里是顺序执行的,一次请求等了 4 秒,体验很糟糕。
- 上下文窗口在第七轮对话时爆掉,报
context_length_exceeded,而前面几轮工具返回的大 JSON 早就该被裁掉。 - 模型给工具传参数时偶尔生成非法 JSON,
call.arguments解析失败,程序卡在json.loads的异常里。 - 同一个错误反复调用同一个工具,每轮都在烧 token,费用肉眼可见地涨。
- 线上出问题后,没有任何日志能告诉你“当时模型看到了什么、工具返回了什么、是哪一步慢的”。
这些问题的本质在于:Agent 循环本身是一条横跨 LLM 调用、工具执行、数据治理的公共链路,而手写代码把这条公共链路的工程细节全部摊在了业务代码里。你写的是业务,却被迫在处理重试、超时、序列化错误、上下文裁剪。这也是为什么很多团队一写 Agent 就陷入“胶水代码爆炸”的泥潭。
1.3 Harness 到底在解决什么问题
先解释一下Harness这个词。它本意是马具,把马和车连接起来,既传递动力,也控制方向。在 LLM 场景里,它是连接“你的业务函数”和“LLM 运行时”的那层装配件。注意它和框架的区别:框架会规定你怎么组织代码,比如必须继承某个基类、必须注册某个模块;而 Harness 的思路是保留你的代码结构,但在边界处注入工程能力。
Strands Agents Harness SDK 要解决的问题非常直接:把 Agent 循环从“业务代码里的一段 while”提升为“平台层的一个可配置组件”。它帮你把重试、上下文管理、并发限制、成本上限、可观测性全部接管,你保留下来的只有两样东西——你要做什么(工具定义、系统提示词、模型选择)和你怎么解释结果(后处理逻辑)。
2. Strands Agents Harness SDK 的核心设计思路
2.1 把“循环”从业务代码里剥离出来
SDK 里最核心的一个抽象叫LoopStrategy(循环策略)。它把“要不要继续、什么时候停、出错怎么办”封装成独立的策略对象,而不是散落在 while 条件里。
看一段配置示例:
from strands_agents import Harness, tool from strands_agents.policies import ( AutoLoop, ExponentialBackoff, TokenBudgetPolicy, MaxCostGuard, )这种设计带来的第一个好处是:你可以为不同场景选择不同的循环策略。比如纯问答场景下,模型基本不会调用工具,那循环轮次设成 3 就够;而数据分析场景里,模型可能需要反复查询数据库、看中间结果、再查下一步,这时就得开到 15 轮以上。手写 while 的话,这种差别只能靠改代码实现;有策略对象之后,配置化就解决了。
第二个好处是循环逻辑可以单独写单元测试。我之前手写循环的时候,想测“工具返回异常时模型会不会换个路子”,得 mock 一整串 LLM 调用;现在直接给AutoLoop传入一个假工具,断言它在异常之后是否触发了重试或降级,一个用例就覆盖住了。
2.2 策略化与可组合性
Harness 把生产级能力拆成了若干策略对象,每个策略只负责一件事,然后通过HarnessPolicy组合:
policy = HarnessPolicy( loop=AutoLoop(max_iterations=12, stop_when_no_tool_calls=True), retry=ExponentialBackoff(max_retries=3, base_delay=1.0, max_delay=30.0), context=TokenBudgetPolicy(token_budget=160_000), cost=MaxCostGuard(max_usd=0.5), concurrency=SemaphorePolicy(max_parallel_tools=3), )每个策略的职责如下:
| 策略 | 负责的事情 | 不负责的事情 |
|---|---|---|
AutoLoop | 决定循环是否继续、最大轮次 | 不关心单次工具怎么执行 |
ExponentialBackoff | 失败后等多久再重试 | 不关心是哪一步失败 |
TokenBudgetPolicy | 上下文窗口怎么裁剪、压缩 | 不改变工具的执行逻辑 |
MaxCostGuard | 检查预估成本,超限熔断 | 不优化 prompt 本身 |
SemaphorePolicy | 限制工具并发数 | 不替代模型做路由 |
这种可组合性的价值在于:你可以像搭积木一样按需装配。内部小工具跑批任务,就把SemaphorePolicy调高、TokenBudgetPolicy调低;外部面向用户的客服 Agent,就反过来把MaxCostGuard加上、把max_iterations收紧。同一个业务 Agent,在不同上线阶段可以换上完全不同的策略组合,而业务代码一行不用改。
2.3 一行代码背后的默认值工程
很多工具的问题不是没有配置项,而是默认值太敷衍。max_retries=0、timeout=None、日志默认关掉,等于把全部责任甩给使用者。Harness SDK 做的第二件重要事情,是把默认值当成一等公民来设计。
它的默认策略大致是这样一套经验值:
max_iterations=8:既容忍多轮工具调用,又防止模型在一个问题上钻牛角尖。max_retries=2:用的是指数退避,第一次失败后等约 1 秒,第二次约 2 秒,避免雪崩式重试。token_budget=160_000:以主流模型上下文窗口的 80% 为上限,留出安全余量。timeout=30s:单次 LLM 调用超过 30 秒直接判失败,不会无限挂起。tracing=True:默认开启 OpenTelemetry span 记录。
这套默认值不是拍脑袋定的。以重试为例:如果重试间隔太短,LLM API 侧的限流还没来得及恢复,重试大概率还是失败;如果间隔太长,用户等得冒火。指数退避是一个在分布式系统里被验证过几十年的策略,直接搬过来用是最稳妥的方案。而 token 预算也是同理,按官方窗口的 80% 设置上限,是因为工具返回结果和对话历史在 tokenizer 统计上存在偏差,留 20% 余量能显著降低context_length_exceeded的概率。
3. 快速上手:一行代码跑通生产级 Agent
3.1 环境准备与安装
SDK 支持 Python 3.10 及以上版本。安装非常常规:
pip install strands-agents如果你用uv管理项目,也可以:
uv add strands-agentsSDK 不绑定具体的模型供应商,只要是 OpenAI 兼容的 Chat Completions 接口都能跑。意味着你既可以用云端服务,也可以指向本地部署的 OpenAI 兼容服务,环境变量OPENAI_API_KEY和OPENAI_BASE_URL配置好就行。我自己测试时就是用本地服务做的离线验证,成本为零,跑熟了再切到云端大模型。
3.2 一个可以直接抄的完整示例
假设我们要做一个股票分析 Agent,它需要两个工具:一个拿实时行情,一个拿 K 线数据。完整代码如下:
from strands_agents import Harness, tool from strands_agents.policies import ExponentialBackoff, TokenBudgetPolicy @tool(description="获取指定股票代码的最新行情报价") def get_quote(symbol: str) -> dict: # 实际项目里这里接你的行情 API return {"symbol": symbol, "price": 418.5, "change_pct": 1.24} @tool(description="获取指定股票最近 N 天的 K 线数据") def get_kline(symbol: str, days: int = 30) -> list[dict]: # 实际项目里这里接你的 K 线服务 return [ {"date": "2025-03-01", "close": 402.1}, {"date": "2025-03-02", "close": 405.8}, # ... ] agent = Harness.create( name="stock_analyst", model="gpt-4o-mini", system_prompt="你是资深股票分析助手,使用工具获取数据后,给出简明且有数据支撑的分析", tools=[get_quote, get_kline], ) result = agent.run("腾讯控股最近一个月的走势如何?值得关注吗?") print(result.text)这四五行就是完整的“从手写循环到一行代码”的落地。agent.run()内部帮你执行完整流程:组装消息、调用模型、判断是否需要工具、并行执行工具、追加工具结果、更新上下文预算、记录全程 trace,最后在模型认为可以收尾时停下来,把最终文本返回给你。
对比一下前面那版手写 while,业务代码量没增加,但行为上多了哪些东西?工具执行失败会被捕获并结构化返回给模型,而不是让程序崩溃;上下文超预算时会自动裁剪最久远的工具结果;每次 LLM 调用都带 30 秒超时;全程产生的 traces 能在观测后端里按一次请求维度查看。这些就是“生产级”三个字的含义。
3.3 关键参数与配置选型
上手的第二个核心问题是参数到底怎么调。我整理了一份基于实测的参数速查表:
| 参数 | 默认值 | 适用场景 | 调参建议 |
|---|---|---|---|
max_iterations | 8 | 多数 QA / 客服场景 | 经验值 5-12,超过 15 说明提示词或工具设计可能有问题 |
max_retries | 2 | 通用 | 对稳定性要求极高的核心链路可以调成 3,但注意成本 |
token_budget | 160000 | 默认推荐 | 只有你的工具结果特别大时才需要调高 |
timeout | 30 秒 | 通用 | LLM 服务响应较慢的网络环境可以放宽到 60 秒 |
max_parallel_tools | 3 | 多工具并行 | 工具本身无依赖时能有效降低总耗时 |
调参的原则我给两条建议。第一,轮次上限是兜底,不是期望值。如果线上 80% 的 Agent 请求都跑到 10 轮以上,不要急着把上限调到 20,应该回头看看是不是工具描述不清楚,导致模型反复试错。第二,成本guard一定要开。MaxCostGuard(max_usd=0.5)这种配置,单次请求成本超限直接熔断并返回结构化错误,能避免 LLM API 异常计费时账单失控。
4. 进阶玩法:把 Harness 真正用进生产环境
4.1 多工具编排与并发控制
当工具数量从一两个涨到十几个之后,会遇到一个手写循环非常难受的问题:模型选错工具、或者反复调用同一个无效工具。Harness 在工具层提供两个实用能力。
第一个是工具可见性控制。你可以给工具打标签,按用户角色过滤工具列表。比如管理员 Agent 能看到“删除用户”工具,普通客服 Agent 看不到。这比在系统提示词里写“你只能使用……”更可靠,毕竟提示词是软约束,模型可能不听话,而工具列表是硬边界。
第二个是并发执行。模型一次返回多个独立工具调用时,SemaphorePolicy可以让它们并发跑,总耗时从“所有工具执行时间之和”降到“最慢工具的执行时间”。实测下来,在 4 个工具并行、单个工具耗时约 800ms 的场景下,总耗时从 3.2 秒降到 0.9 秒,体验改善非常明显。代价是需要自己保证工具之间没有共享可变状态,否则并发会引入竞态问题。
4.2 可观测性与调试技巧
生产级 Agent 最大的隐性需求是可观测性。SDK 默认通过 OpenTelemetry 协议输出 traces,一次agent.run()调用会打开一组嵌套 spans:
| Span 名称 | 记录的字段 | 调试价值 |
|---|---|---|
agent.run | 输入 query、最终输出 | 一次请求的全景 |
llm.call | model、input_tokens、output_tokens、耗时 | 判断每次模型调用的成本和延迟 |
tool.execute | 工具名、参数、返回值大小、异常信息 | 定位慢工具和失败工具 |
context.trim | 裁剪的 token 数、剩余预算 | 上下文治理是否生效 |
之前线上排查一个“Agent 答非所问”的问题,我打开 trace 发现某次tool.execute返回了一个 2 万字符的超大 JSON,接着上下文裁剪把前面对话摘要删掉了,模型丢失了关键背景信息。如果没有这层 traces,这种问题只能靠猜。本地调试时可以把 traces 导出到控制台或 Jaeger,也可以在测试环境里接入任意 OTLP Collector。
还有一个非常实用的回放技巧:SDK 支持把一次真实请求的 trace span 序列化成 JSON 文件,在本地用回放工具逐步查看每一步的输入输出。这比打印日志高效得多,尤其是当模型输出格式飘忽不定的时候,你能清楚看到“模型到底是在第几步开始跑偏的”。
4.3 与现有代码库的集成
大部分读者手里已经有一套业务系统,不太可能为了引入 Agent 把所有代码重写。Harness 在设计上比较克制,集成的侵入性很低。
在 FastAPI 服务里,Agent 可以当作一个普通依赖注入使用:
from fastapi import FastAPI, Depends from strands_agents import Harness app = FastAPI() analyst_agent = Harness.create( name="stock_analyst", model="gpt-4o-mini", tools=[get_quote, get_kline], ) @app.post("/analyze") async def analyze(query: str, agent: Harness = Depends(lambda: analyst_agent)): result = await agent.arun(query) return {"answer": result.text, "trace_id": result.trace_id}注意这里用的是arun异步接口,生产环境下不阻塞事件循环。并发量大时还可以给每一个请求生成独立的会话上下文,而不是让所有用户共享同一个历史消息列表。
另外,SDK 也支持子 Agent 组合。主 Agent 在分析复杂问题时,可以把“数据清洗”或“报告生成”拆成独立的子 harness,主 Agent 通过标准工具调用触发子 Agent。这种组合方式比把所有指令塞进一个超长系统提示词更可控,每个子 Agent 的循环策略可以单独收紧。
5. 常见问题与排查技巧实录
5.1 工具调用失败导致的死循环
这是我自己踩过最深的坑。现象是某次线上事故中 Agent 在一个工具上报错后,模型不断尝试调用同一个工具,直到max_iterations打满,token 费用直接翻了三倍。
排查后发现:工具抛出的异常是原始的 Python traceback,里面全是内部文件路径和调用栈,模型根本看不懂发生了什么,只能靠继续调用同一个工具来尝试“修复”。解决方式是给工具执行加一层“失败结构化转换”——把异常变成模型能理解的一段话,比如“获取行情失败:上游超时,请稍后重试或改用备用数据源”。模型看到明确的失败原因后,会自然地选择其他路径或告知用户暂时不可用,而不是机械重试。这个过程其实就是给 Agent 一条“体面的退路”。
5.2 上下文窗口爆掉的排查
另一个高频事故是context_length_exceeded。第一次遇到时我以为是模型窗口不够大,连续调高了两个档位,结果费用直线上升。
后来通过 traces 发现,问题来自某个工具返回了一个巨大的 CSV 数据,被原样塞进了对话历史。这种场景下,增大窗口只是给问题买时间——数据量到一定程度照样会爆。更合理的做法是:
- 给工具返回加
max_size限制,超出部分截断或摘要。 - 对超大工具结果在进入历史之前先做内容压缩,只保留与用户问题直接相关的字段。
- 开启
TokenBudgetPolicy的自动裁剪机制,让最旧的工具结果优先被清除。
这三个方案里,第一和第二是从源头减少 token,第三个是兜底。我现在的经验是三个一起上,靠单一手段都不可靠。
5.3 生产环境的避坑清单
最后分享一张整理过的速查表,基本涵盖了新上线 Agent 项目最容易踩的坑:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| Agent 反复调用同一个失败工具 | 异常信息没有结构化,模型看不懂 | 失败时返回明确原因 + 备选建议 |
| 并发下工具结果互相污染 | 工具内使用了共享可变全局变量 | 工具函数保持无状态,或使用上下文隔离 |
| 上下文突然超限 | 工具结果未经裁剪直接入历史 | 设置max_size+ 自动裁剪 |
| 单次请求成本超预期 | 没有成本上限,循环轮次失控 | 开启MaxCostGuard,设置硬性熔断 |
| 本地正常、线上超时 | 本地模型快,云端模型慢 | 合理设置timeout,把重试间隔调成指数退避 |
| 线上问题无法定位 | 没有日志和追踪 | 开启 OpenTelemetry,保留 trace 回放文件 |
一个额外的建议是:工具必须设计成幂等的。尤其是带写入操作的工具,重试机制会放大重复执行的副作用。如果工具本身幂等性做不到,至少要在工具内部做一个同参数去重,防止同一次请求内的重复调用产生订单、扣款之类的二次副作用。
我个人在实际操作中的体会是,使用 Harness SDK 之前,我对 Agent 代码总有一种“什么都要自己掌控”的执念,觉得循环逻辑写在眼皮底下才安心。直到线上账单和事故把我教育了一遍,我才意识到:工程化能力不是靠意志力堆出来的,而是靠被验证过的策略组件组合出来的。现在写一个新 Agent,我反而会刻意少写循环、少写 try/except,把这些都交给策略层,把精力放在调工具描述、调提示词、看 trace 回放这些真正影响模型行为质量的事情上。
最后再分享一个小技巧:每次发布 Agent 新版本之前,我都会拿 SDK 回放功能把线上最近一段时间有问题的 trace 重跑一遍,重点看两个指标——工具调用平均轮数、单次请求平均 token 消耗。这两个数字如果比上一版涨了 20% 以上,我基本就知道是提示词改动引入了歧义,赶紧调回去。这个习惯帮我避免了好几次“感觉改了更好,实际越改越差”的发布事故。希望这个思路对你的 Agent 上线也有帮助。