返回博客列表

Strands harness 深度解析:一次 import 拿到成品 Agent,默认值里藏了什么

2026-10-04T14:00:00+08:00
Strands harnessAgentAWSHarnessMCP开源

Strands harness 深度解析:一次 import 拿到成品 Agent,默认值里藏了什么

"拼装 Agent"这件事,终于有人把默认值也一起交付了。

过去一年做 Agent 的人大概都写过同一套代码:接一个 shell、加读文件写文件的工具、想办法把越来越长的对话塞回上下文窗口、再补一个跨会话的记忆、顺手写个审批钩子。每个项目都要重来一遍,而且每一遍的完成度都不一样。

Strands harness 想省的正是这一步。它的承诺很短:一次 import,返回一个已经调好的 agent——工具、上下文管理、会话、记忆、hooks,加上一份调过的 system prompt 都在里面。文档把它叫"state-of-the-art, fully assembled agent harness"。

但"开箱即用"这四个字在 Agent 领域经常是陷阱:默认值不透明,出事时你不知道该关哪个开关。所以这篇文章不重复 API 列表,而是回答三个问题:它默认到底装了什么(具体到配置项、目录名和默认值)、为什么这样设计、以及什么时候它会反过来咬你一口。

本文提纲

  1. 问题:为什么会有 harness 这一层
  2. 一次 import,默认 harness 里装了什么
  3. 三个最值钱的默认值:上下文、记忆、会话
  4. 模型与 reasoning:一个字符串 + 一个 effort
  5. 工具层:内置、自定义与 programmatic_tool_caller
  6. 子 Agent 与干预:风险在哪里被拦住
  7. 失效边界:什么时候不要用它
  8. 三种上手路径

问题:为什么会有 harness 这一层

先把"harness"这个词摆正。直译是马具——缰绳、马鞍、护具。它不负责让马跑得快,负责让马跑得稳、听指挥。放到 Agent 上就是那句话:模型负责聪明,harness 负责稳。

业界的存在两种做法。LangGraph、CrewAI 这类框架走的是"给你一堆零件,自己拼":灵活,代价是每个项目的 harness 质量参差不齐。Strands harness 走的是另一种:**先把一套调好的默认值交付给你,再让你逐项覆盖。**官方对它的定位描述得很准确——"opinionated in its implementation but not restrictive":观点鲜明,但不限制你。

有一个设计决定值得单独说:**Strands harness 返回的不是一个包装类,而是一个标准的 Strands Agent,没有 wrapper、没有隐藏抽象层。**这一点决定了后续所有事情——SDK 的部署指南、可观测性、Evals 评测、安全文档,都能原封不动地用在它身上,不需要"Strands harness 专属"的版本。很多"全家桶"式框架栽在这里:用得越深,越难出去。

Strands 整个工具箱的分层是清楚的:

  • Strands harness:成品,一次调用拿到 agent,默认值齐全。
  • Strands Harness SDK:地基,要完全自己造 harness 时直接用它,配置和你已经在用的那套一样。
  • Strands Shell / Strands Evals:配套,前者是给 AI agent 用的虚拟 shell,后者负责上线前验证。

一次 import,默认 harness 里装了什么

最短的上手代码就是三行:

# pip install strands-harness
from strands_harness import create_harness

agent = create_harness()
agent("Research the top three vector databases, compare pricing and limits, and write it up in comparison.md")

TypeScript 版本形状一致:

// npm install @strands-agents/harness
import { createHarness } from '@strands-agents/harness'

const agent = await createHarness()
await agent.invoke('Research the top three vector databases, compare pricing and limits, and write it up in comparison.md')

create_harness() 什么都不传,你拿到的默认 agent 是这些:

维度 默认行为
模型 bedrock/global.anthropic.claude-opus-5,reasoning 打开
系统提示词 调过的 contract:先探索再动手、不可逆操作前先确认、结束前先验证
内置工具 shell、read、write、edit、web_fetch、web_search、programmatic_tool_caller、subagent
内置插件 todos(多步任务清单)、environment
上下文 context_manager="auto",含大体积工具结果 offload
缓存 caching="auto",开启
会话 开启,状态落在 ./.agent/sessions
长期记忆 开启,文件落在 ./.agent/memory
Skills 扫描 ./.agent/skills
子 Agent 内置一个 generalist
干预 无,所有工具调用直接放行

把这份表放在一起看,你会发现默认值的取向是"先让它能干活,再谈收窄":能跑 shell、能改文件、能联网、能自己写代码调工具。安全相关的默认值(干预)留空,这既是信任,也是上线前必须自己补的一课,后面单独讲。

三个最值钱的默认值:上下文、记忆、会话

这三项是"自己拼"时最容易做残、也最难做对的部分。

上下文管理:把胖工具结果请出窗口

context_manager="auto" 打开后,harness 做两件事。一件是随对话增长摘要化更早的轮次,让长任务不撑爆窗口;另一件更关键——把大体积的工具结果替换成"短预览 + 一个引用",完整内容留在 context manager 的 stash 里,agent 真正需要时用 retrieve_context 工具读回来。

这解决的是"真正的问题"。Agent 上下文爆炸通常不是聊天记录太长,而是某次搜索或某次读文件吐了几万 token 的原始数据,而模型其实只用得上其中三行。

档位有三个:"auto"(默认)、"agentic",或者关掉。**这里有个反直觉的细节:关掉上下文管理会连带关掉 offloading。**不是"我只是不想让它摘要",而是"完整历史全留在窗口里,窗口大小由你自己负责"。如果真要关,先确认你的任务规模。

还有一条和会话耦合的行为:会话激活时,被 offload 的内容会落盘到会话目录;没有会话,它们只留在内存里,进程结束就没了。

记忆:Markdown 文件 + 后台蒸馏

记忆默认开着,落在 ./.agent/memory,是一堆纯文件,不依赖任何会话——所以会话关掉它也照样工作。

它的工作方式是:每轮开始前搜索 store,把最相关的条目注入上下文;同时给 agent 一个 search_memory 工具按需召回。而"从对话里蒸馏出持久事实"这件事在后台每几轮跑一次,用的是一个小号、凭据对齐(credential-aligned)的模型——这是它成本低的原因,也是它在后台默默烧 token 的地方。

有个容易忽略的细节值得记住:**记忆注入每轮都跑,不只是用户说话的那一轮。**多步任务里的每一步、以及委派出去的子 agent,每一轮都会读到记忆。

想换后端可以传 memory={"stores": [...]},但策略仍由 harness 掌握:注入照开、search_memory 照给、不提供写工具。想彻底换掉管理器,就传一个完整的 memory_manager 进去,harness 会让位。另外内置的 generalist 子 agent 对这些 store 是只读共享——它能召回同样的记忆,但不会把子任务的临时噪音写进去。这个设计挺聪明:子 agent 最容易污染长期记忆。

会话:一个 id 就是断点续传

agent = create_harness(session={"id": "refactor-parser"})
agent("Let's refactor the parser. Where should we start?")

# ...另一个进程里...
resumed = create_harness(session={"id": "refactor-parser"})
resumed("Where did we leave off?")

同一个 id 就恢复,新 id 就重开。id 会被规范化成小写字母数字加连字符、下划线,所以 "Refactor Parser" 和 "refactor-parser" 指向同一个会话——这个细节能省掉一类"为什么没续上"的排查。

模型与 reasoning:一个字符串 + 一个 effort

model 接受三种形式:provider/name 字符串、裸的 Amazon Bedrock model id、或者一个已经配置好的 Strands Model 实例。provider 前缀有别名:bedrock、bedrock-mantle、anthropic、openai、google、ollama、litellm。

agent = create_harness(model="anthropic/claude-opus-5", effort="high")

拼错的 provider 前缀在构造时就报错,并列出支持的 provider——而不是等你发请求时才炸。这种"把错误提前到构造期"的处理方式,在整份配置里反复出现(未知的 builtin_tools 名字、工具重名、provider 拒绝某档 effort,都是构造期报错)。

effort 把一档 reasoning 级别映射到各家 API 各自的写法,所以不管你用哪家,都只设一次:"auto"(默认,用 provider 推荐档)、"low"/"medium"/"high",或者 off。传 Model 实例时 effort 会被忽略——因为这时候模型已经由你自己配好了。

工具层:内置、自定义与 programmatic_tool_caller

自定义工具就是普通的 Strands tool,直接 tools=[...] 传进去,和内置工具并列:

from strands import tool
from strands_harness import create_harness

@tool
def get_ticket(ticket_id: str) -> str:
    """Fetch a support ticket by id."""
    return f"Ticket {ticket_id}: open, assigned to support."

agent = create_harness(
    instructions="You are a support assistant. Always link the ticket you acted on.",
    tools=[get_ticket],
)

工具名必须跨所有来源唯一(内置工具、你的工具、插件提供的工具),重名在构造期就失败。

instructions 和 system_prompt 的关系值得记牢:instructions 是一个追加在 harness contract 之后的领域块——agent 的身份、范围、规则。如果你直接传完整的 system_prompt,instructions 会被忽略,因为你提供的提示词整体替换了 contract。harness 管的"怎么像一个 agent 那样行动",你管的"它是谁、为什么存在",这条分工线划得很清楚。

默认打开里最特别的是 programmatic_tool_caller:模型不逐个调工具、等结果、再调下一个,而是写一小段 Python,把其他所有工具当成 async 函数来用——可以链式调用、循环、过滤、并行,全在一个 turn 里完成。只有代码 print 出来的内容回到模型,工具的返回值除非打印,否则留在代码的局部作用域。这正是"调 20 个工具"被压缩成"一条结论"的机制:模型可以读十个文件、只留下匹配项、打印一段摘要,而十个文件的全文从未进入上下文窗口。

隔离边界要说清楚,否则容易误判安全模型:这段代码跑在 Monty 里——一个隔离的 Python 解释器,里面没有文件系统、环境变量、网络和进程访问,模型写的代码唯一能碰到的东西就是传进去的那组工具函数。Monty 跑在独立 worker 进程,有内存、时间、递归限制,输出有上限(防止失控的 print 撑爆上下文),await 的代码有默认超时。

但文档自己把边界标得很明确:**Monty 约束的是代码,不是代码调用的工具——shell 依然在宿主机上执行命令。**要给工具本身一个 OS 边界,得把 agent 放进 SDK 的 sandbox 里。

还有一条和干预的交互:基于 interrupt 的人工审批没法从代码里弹窗,所以那种被拦截的调用会抛出可捕获的错误,而不是停下来等人点确认。Cedar 或自然语言风险策略这类"自治"策略则正常工作,代码会收到错误并可以自己处理。

子 Agent 与干预:风险在哪里被拦住

子 agent 有两种加法。一种是你自己的 specialist:把 Agent 用 as_tool() 包一下传进 tools,每个子 agent 的工具名取它自己的 name。另一种是内置的 generalist。

两者的行为差异很实际:specialist 每次调用都从构造时的基线开始,是全新对话,不跨调用累积状态;generalist 则继承父级配置(model、thinking、caching、上下文管理、内置工具、plugins、subagents、interventions、sandbox),但用通用角色提示词、从空白对话开始。也就是说,generalist 干活时看不到你主对话的历史,子任务需要的一切都得塞进这一次调用。它的适用场景是"否则会淹没主上下文"的活儿:翻很多文件、多步改动、开放式探索——你只要结论。

干预(interventions)是上线前最该补的那块。默认是什么都不拦,每个调用都放行。打开的方式有几种:

  • interventions="ask":每个工具调用都要批准。
  • interventions="smart":用 SDK 的风险分类器判断,只拦有风险的那些。
  • 一段自然语言策略:任何不是 preset、也不以 .cedar 结尾的字符串都会成为风险分类器的 prompt,让模型按你的标准判断并升级审批。
  • 一个 Cedar 策略文件,或者 SDK 的 handler 实例,或者上述的列表。

返回的是类型化动作:Proceed、Deny、Confirm(停下来问人)、Guide(把这一轮交还给模型并附反馈,而不是硬拦)。

失效边界:什么时候不要用它

默认值偷懒的地方,就是出事的地方。以下几条是文档明确点出的,也是我最认同的部分:

**跑容器或 Serverless 时,默认的落盘位置会骗你。**会话和长期记忆默认写在 ./.agent 下,容器实例一重建就没了。要么用 session={"dir": ...} 和 memory={"dir": ...} 指向持久卷,要么换成自定义 store 后端。

**处理不可信输入时,别只靠 Monty。**前面说过,它隔离代码不隔离工具。要么把 agent 放进 SDK sandbox,要么直接把这个工具从 builtin_tools 里删掉。

**默认 agent 能跑 shell、能改文件。**这不是 bug,是它的默认取向。生产前至少要配一层干预,再用 sandbox 收窄它够得着的范围。

**缓存不是所有 provider 都能"由 harness 帮你省"。**在 Bedrock 和 Anthropic 直连上,harness 会配置 cache point 和 cached tool definitions;在 OpenAI、Google、bedrock-mantle 上,缓存是服务端自动的,harness 没什么可配。所以 caching="auto" 在不同 provider 上的实际含义不一样。

反过来,它的 sweet spot 也很清楚:你要的是一个能干活、默认合理、并且随时能掀开盖子改的通用 agent——研究、写文件、多步任务、跑一段代码处理数据。如果任务本身就是"极致确定性的固定流水线",或者你需要对每一轮提示词做 pixel 级控制,那你要的其实是 Strands Harness SDK 或者别的框架,不是这个成品。

三种上手路径

**路径一:CLI,不写代码。**Node.js 20+ 起步:

npm install -g @strands-agents/cli
strands

Quickstart 只问一件事——模型 provider(Amazon Bedrock、Anthropic、OpenAI、Google Gemini、Ollama、LiteLLM),确认凭据、选模型,然后 Save and Launch 直接进聊天。想改名字、instructions、工具、插件、记忆和工具权限,走 Customize。等你要嵌进应用了,在聊天里 /export(或 setup 里的 Export)会导出一个 Python 或 TypeScript 项目,你的选择直接落在 create_harness(...) / createHarness(...) 的调用参数上。也可以跳过 setup 直接从导出的文件启动:strands --agent ./agent.ts。

**路径二:当库用。**Python 3.10+ 装 strands-harness;TypeScript 在 Node.js 20+ 下装 @strands-agents/harness。provider 靠 provider/name 字符串选,Bedrock 是默认所以不用传 model;Ollama 本地跑不需要 key。

**路径三:让编码 Agent 带你配。**文档给了一段可直接粘进 Codex、Claude Code、Kiro 的提示词,它会先问你想走 CLI 还是库、Python 还是 TypeScript、用哪个 provider,再按步骤来。另外 Strands 提供了一个 MCP server,让编码工具在工作时能读到最新的 Strands 文档:

uvx strands-agents-mcp-server

配置位置各家不同:Kiro 在 ~/.kiro/settings/mcp.json、Cursor 在 ~/.cursor/mcp.json,Claude Code 直接 claude mcp add strands uvx strands-agents-mcp-server,Codex 写 ~/.codex/config.toml,VS Code 用 mcp.json。这不是可有可无的一步——模型记忆里的 API 往往落后于文档版本,让编码 Agent 读实时文档能少踩一类"照着旧 API 写"的坑。

参考文档与链接

你会直接用默认 harness 一路跑下去,还是从 Harness SDK 自己拼一套?评论区聊聊你的取舍。觉得有用点个赞,让更多在拼 Agent 的人看到。


作者: itech001 来源: 公众号:AI人工智能时代(the-ai-era) 网站: https://www.theaiera.top/ 关注每日最新AI新闻和技术博客,主页有更多的文章的AI 技术参考:https://www.theaiera.top

本文首发于 AI人工智能时代,转载请注明出处。

分享给朋友