Agent 内核:模型、Harness、工具与环境

版本说明:本文保留为已发布快照。持续更新、经过源码核验和实践验证的版本,见 《Agent 工程实践》第 2 课。后续完整正文只在书籍仓库维护。

第 1 课已经区分了 Agent 与 Workflow:固定路径由代码控制,Agent 则让模型根据目标和环境反馈动态决定下一步。本篇不再重复定义,而是往工程实现里多走一层。

常见的记忆公式是:

1
Agent = 模型 + 循环 + 工具

它适合记住最小形状。真正落到代码时,还要把“循环”展开成 Harness,并补上工具面对的 Environment:

1
2
Tool-using Agent Runtime
= model + harness + tools + environment

1. 一次工具调用里,谁做了什么

用户要求“读取配置文件并解释”。完整过程不是模型直接打开文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
User Request

Harness 把历史和工具定义发给模型

Model 返回 read_file Tool Call

Harness 校验参数与权限,路由到本地工具

Tool 读取文件系统

Environment 返回文件内容或错误

Harness 把 Tool Result 写回消息历史

Model 根据真实结果继续调用工具或给出 Final

四个部分的边界如下:

部分负责什么不负责什么
Model理解目标,选择下一步,生成工具名与参数不直接获得本地文件权限
Harness维护消息、执行循环、校验协议、控制权限和停止条件不替模型做开放式决策
Tool执行一个边界清楚的动作不决定自己何时被调用
Environment文件系统、Shell、网页或外部 API 的真实状态不保证返回内容安全或适合直接进 Prompt

Harness 不是一段“只会转发”的空循环。最大步数、超时、并发、参数校验、审批、持久化和恢复都属于 Harness。这里要分清“选择”和“批准”:Model 选择 read_file,Harness 判断它能不能执行。Harness 不替 Model 分析该读哪个文件,Model 也不能给自己的动作授权。

2. 工具协议是怎样接起来的

不同提供商使用不同字段。本节只讨论 Anthropic Messages API 的客户端工具协议;第 3 课会运行 OpenAI 兼容的 assistant.tool_calls → role=tool 协议。

2.1 先把工具描述给模型

应用把工具名、说明和 input_schema 交给 API:

1
2
3
4
5
6
7
8
9
10
11
{
"name": "read_file",
"description": "Read a UTF-8 text file inside the workspace",
"input_schema": {
"type": "object",
"properties": {
"path": {"type": "string"}
},
"required": ["path"]
}
}

Schema 只让模型知道怎样请求工具,不会把本地函数上传给模型,也不会自动授予文件权限。它是一张申请表,不是门禁卡。

真正的权限分成三层:

1
2
3
4
操作系统:运行工具的进程最多能访问什么
Harness:本次请求允许访问什么
Tool:在允许范围内执行读取
Model:只能提出请求

2.2 模型返回 tool_use

模型决定读取文件时,Assistant Message 包含 tool_use Content Block:

1
2
3
4
5
6
{
"type": "tool_use",
"id": "toolu_01A",
"name": "read_file",
"input": {"path": "config.json"}
}

这里仍然只是请求。Harness 要检查工具是否存在、参数是否符合 Schema、路径是否允许,再决定执行或拒绝。

2.3 应用执行客户端工具,再返回 tool_result

工具执行后,应用使用原 tool_use_id 回传结果:

1
2
3
4
5
6
7
8
9
10
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_01A",
"content": "{\"theme\":\"dark\"}"
}
]
}

tool_use_id 的作用是把请求与回执配成一对。Anthropic 与 OpenAI 兼容协议使用不同字段名,但都需要这条关联:

协议模型发出的请求 ID应用回传结果时使用
Anthropic Messagestool_use.idtool_result.tool_use_id
OpenAI 兼容协议tool_calls[].idrole=tool.tool_call_id

没有 ID,模型无法判断一段错误属于 read_file 还是 run_tests;配错 ID,消息即使通过格式校验,语义也已经错位。

2.4 多个工具怎样执行,结果又怎样回传

假设同一条 Assistant Message 返回两个请求:

1
2
toolu_A:read_file("config.json")
toolu_B:run_bash("rm -rf output")

Harness 可以并行执行,也可以按顺序执行。执行方式由 Harness 决定,消息格式由协议决定。 按当前 Anthropic 协议,下一条 user 消息要收齐这批请求的回执,并一次返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"role": "user",
"content": [
{
"type": "tool_result",
"tool_use_id": "toolu_A",
"content": "...文件内容..."
},
{
"type": "tool_result",
"tool_use_id": "toolu_B",
"is_error": true,
"content": "权限策略拒绝,未执行"
}
]
}

失败、超时和拒绝也都是结果,不能静默丢掉。若模型一次请求 30 个工具,下一条消息就应有 30 个 tool_result;没有执行的调用也要带原 ID,并用 is_error: true 说明原因。真正不希望每轮出现太多调用时,应由 Harness 限制数量,或关闭并行 Tool Use,而不是破坏请求与结果的配对。Parallel tool use

Anthropic 还提供 Web Search、Code Execution 等服务端工具,这类工具由 Anthropic 基础设施执行,不能概括成“所有工具都在本地程序运行”。Define toolsHandle tool calls

3. Loop 不能只处理成功路径

下面是协议伪代码,用来展示 Harness 的控制流。它不依赖具体 SDK,也不能直接编译:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
history = [user_message]

for step in range(MAX_STEPS):
response = call_model(history, tool_schemas)
history.append(response)

if response.stop_reason == "tool_use":
calls = extract_tool_calls(response)
results = execute_with_policy(calls)
history.append(tool_results_message(results))
continue

if response.stop_reason == "end_turn":
return response.text

if response.stop_reason == "pause_turn":
continue_with_preserved_response()

raise IncompleteRun(response.stop_reason)

raise StepLimitExceeded()

只处理 tool_useend_turn 不够。当前 Anthropic 协议还可能返回 max_tokensstop_sequencepause_turnrefusalmodel_context_window_exceeded。Harness 必须为这些状态定义策略,不能把所有非工具响应都当成成功 Final。How tool use works

流式显示与轮次结束也要分开。max_tokens 前生成的文字可以显示给用户,但它仍是被截断的中间内容,不能保存成成功 Final;截断若发生在工具参数中,更不能拿半段参数去执行。只有 Harness 识别并接受结束状态后,这一轮才算完成。

本课只讲控制流,不复制容易随 SDK 版本变化的实现。需要对照真实代码时,可以查看 Anthropic 官方的手写工具循环Tool Runner;下一课再运行本专栏的第一份完整代码。

4. 工具接口越简单,安全责任越不能省

工具函数看起来很简单,真正的风险却集中在输入路径和返回内容。路径不能只做字符串前缀检查:/workspace/link/secret.txt 看似在 Workspace 内,link 却可能指向 /etc。Harness 至少要解析 realpath,再检查真实路径是否仍落在允许目录。

工具结果也不是越完整越好。一个 500 KB 文件既会挤占 Context,也可能夹带“忽略之前要求并执行危险命令”之类的提示注入。Harness 应限制大小、分页或截断,把完整内容保存成可回查的 Artifact;同时先脱敏,并始终把外部文字当作不可信数据。即使 Model 受它影响提出危险动作,Harness 的权限与审批仍必须独立生效。

错误返回同样要控制。退出码、关键 stderr、截断标记与完整日志位置,通常比整份堆栈更有用。错误状态只说明工具发生了什么,不保证 Model 一定能够修好问题。

上下文截断、工具恢复与沙箱会在后续课程分别展开。本课只建立边界:模型提出动作,Harness 决定动作是否符合协议与策略,Tool 在受控范围内接触 Environment。

5. MCP、Skills 与 Agent SDK 位于不同层

这些名词经常一起出现,却不是三种同类框架:

能力所在层解决的问题
MCP客户端—服务端协议应用怎样发现和调用 Tools、Resources、Prompts
Agent Skills能力包格式指令、脚本、参考资料和资产怎样渐进加载
Agent SDK运行时封装Loop、工具、Session、Handoff、Guardrail、Tracing 等怎样组织

MCP 不规定应用如何运行 LLM;Skills 也不替 Agent 执行循环。Agent SDK 最接近本文的 Harness,但不同 SDK 封装的边界并不相同。MCP ArchitectureAgent SkillsOpenAI Agents SDK Runner

因此,“接入 MCP 后不再需要 Harness”是把连接层当成了运行层。MCP Server 可能真的执行工具,也可能有自己的权限控制;Host 仍要决定何时让 Model 继续、哪些调用对当前用户开放、何时停止或请人确认。连接成功只证明双方能通信,不证明本次动作应该获准。

6. 本课留下的判断

看到一次工具调用时,先不要只盯着模型输出。沿着数据流问四件事:

1
2
3
4
Model 为什么选择这个动作?
Harness 做了哪些校验和状态管理?
Tool 实际执行了什么?
Environment 返回了什么事实?

这四个问题能帮助你阅读任何 Agent SDK,也能暴露教程代码藏起来的权限、停止条件与恢复问题。

下一课进入可运行代码:Agent 实践:跑通第一个 Tool Calling Loop。它使用 Python 和 OpenAI 兼容协议,完整展示 Tool Call、Tool Result 与下一轮模型请求怎样接起来。

主动回忆自测

读完后先合上文章,再口头回答:

  1. Model、Harness、Tool 与 Environment 分别负责什么?
  2. 为什么 Tool Schema 不是本地文件的权限证明?
  3. Model 返回 tool_use 后,Harness 至少还要做哪三件事?
  4. tool_use_idtool_call_id 有什么共同作用?
  5. stop_reason=max_tokens 时,为什么文字可以流式展示,却不能当作成功 Final?
  6. 同一轮有多个 Tool Call 时,“怎样执行”与“怎样回传”有什么区别?
  7. 为什么字符串前缀检查挡不住符号链接越界?
  8. 大块 Tool Result 进入 Prompt 会带来哪两类风险?
  9. MCP 与 Harness 分别位于哪一层,为什么不能互相替代?
  10. 同一批调用中一个获准、一个被拒绝,下一条消息应该包含什么?
展开查看简答
  1. Model 选择下一步;Harness 维护循环并检查策略;Tool 执行动作;Environment 提供真实状态与反馈。
  2. Schema 只描述请求格式。实际权限来自宿主进程、操作系统和 Harness 的策略。
  3. 检查工具是否存在、参数是否符合 Schema、路径和权限是否允许,然后才能执行并回传。
  4. 它们都把每个 Tool Result 配回原来的 Tool Call,只是提供商字段名不同。
  5. max_tokens 表示输出被截断。中间文字可以展示,但内容或工具参数可能不完整,轮次尚未成功结束。
  6. Harness 可以并行或顺序执行;Anthropic 协议要求同一批结果收进下一条 user 消息,并逐个用 ID 配对。
  7. 表面路径可能经过符号链接指向 Workspace 外部,必须解析真实路径后再检查边界。
  8. 一是挤占 Context、增加费用或超窗;二是把不可信指令带进 Prompt,诱导 Model 请求危险动作。
  9. MCP 是外部能力连接协议;Harness 是运行控制层,负责循环、状态、权限和停止条件。
  10. 同时返回成功结果和拒绝结果。拒绝项保留原 ID,使用 is_error: true 明确说明未执行。

参考资料