用TypeScript循环拆解Agent执行框架
文章用一个简短TypeScript循环解释Agent执行框架:向模型提供工具定义、接收结构化调用、检查权限、执行工具并回传结果。它同时说明最终回答、运行限制、错误或取消如何结束循环。
AI 深度解读
AI 编程工具为什么能读文件、改代码、跑测试?原文给出的解释是:模型负责提出下一步,harness 负责把请求变成实际操作,再把结果交回模型。理解这一层,比记住几个 Agent 产品名更有用,因为权限、上下文和停止条件都在这里落地。
作者用一段 TypeScript 循环拆开了这个过程。它适合拿来理解 Agent 的运行机制,但还不能直接当生产模板:示例只经过类型检查和本地模拟 API 验证,没有调用真实 API,文中的模型与接口兼容性说法也要带着这个前提看。
模型提出请求,谁来真正执行?
作者把 harness 理解为模型与工具之间的运行层。模型自己不能打开文件、执行命令或点击按钮;它输出工具请求,由外部程序接住并执行。
以 Claude Code 修复失败测试为例,模型可能依次请求读文件、修改文件、运行 npm test,再根据测试输出决定是否继续修改。真正接触文件系统和进程的是工具及其运行环境。不过,选择改哪里、执行什么命令,仍然由模型参与决定。
这个运行层承担四件事:
- 告诉模型有哪些工具,每个工具包含名称、描述和输入 schema。
- 从响应中识别工具调用,并分发给对应处理程序。
- 检查调用是否允许执行,再运行工具,也可以拒绝。
- 把结果交回模型,继续循环,直到正常结束或被其他条件中止。
图里的 <bash cmd="npm test"> 只是演示记号。真实 API 会提供结构化数据,无须从回答正文里搜索一段像命令的文字。
在 Anthropic Messages API 中,请求是 tool_use 块,带有 id、name 和 input。假设工具叫 bash,参数可以是 { "command": "npm test" }。执行后,程序通过 user 消息中的 tool_result 返回输出,用 tool_use_id 对应原来的调用,例如 toolu_01。
每次调用都要有对应结果,同一消息中的工具结果要排在其他文本前面。OpenAI Responses API 使用另一套名称:function_call 携带 call_id,执行方返回 function_call_output。
工具调用是一轮需要程序接力的交互,不能看到模型输出文字就认为任务已经完成。
一段循环里,最容易写错的是历史和退出条件
作者选择时钟作为工具,避免示例一开始就涉及 shell 权限。代码使用 @anthropic-ai/sdk,需要环境变量 ANTHROPIC_API_KEY,并以 ES module 运行。
下面这段代码展示了工具声明、参数检查、执行和结果回传的完整闭环:
import Anthropic from '@anthropic-ai/sdk'
const client = new Anthropic() // reads ANTHROPIC_API_KEY
// Lists tools: what the model may ask for. One harmless tool here.
const tools: Anthropic.Tool[] = [
{
name: 'get_time',
description: 'Get the current UTC date and time.',
input_schema: { type: 'object', properties: {}, additionalProperties: false }
}
]
// Runs code: only the names you handle can run.
function runTool(call: Anthropic.ToolUseBlock): Anthropic.ToolResultBlockParam {
const input = call.input
if (
call.name !== 'get_time' ||
input === null ||
typeof input !== 'object' ||
Array.isArray(input) ||
Object.keys(input).length !== 0
) {
return {
type: 'tool_result',
tool_use_id: call.id,
content: 'Unknown tool or invalid arguments',
is_error: true
}
}
return { type: 'tool_result', tool_use_id: call.id, content: new Date().toISOString() }
}
async function runAgent(prompt: string): Promise<string> {
const messages: Anthropic.MessageParam[] = [{ role: 'user', content: prompt }]
for (let turn = 0; turn < 10; turn++) {
const response = await client.messages.create({
model: 'claude-opus-5-5',
max_tokens: 16000,
tools,
messages
})
// Keep every block, thinking included, exactly as it came back.
messages.push({ role: 'assistant', content: response.content })
if (response.stop_reason === 'end_turn') {
return response.content
.flatMap((block) => (block.type === 'text' ? [block.text] : []))
.join('')
}
if (response.stop_reason !== 'tool_use') {
throw new Error(`Stopped: ${response.stop_reason}`)
}
// Catches calls: the tool_use blocks in the response.
const calls = response.content.filter(
(block): block is Anthropic.ToolUseBlock => block.type === 'tool_use'
)
// Returns results: one for each call, all in one user message.
messages.push({ role: 'user', content: calls.map(runTool) })
}
throw new Error('Too many turns')
}
console.log(await runAgent('What time is it in UTC?'))
最该盯住的是 content: response.content 这一行:它把完整响应放回历史,没有只提取文本后重建对话。
作者称 Claude Opus 5.5 总会进行 thinking,相应块必须原样回传。无论具体模型如何要求,这里要理解的工程问题都是:API 返回的结构可能包含后续请求依赖的内容,不能只保留界面上看得到的答案。
退出判断同样关键。stop_reason === 'end_turn' 才返回最终文本,tool_use 才继续执行工具;遇到 max_tokens、refusal,以及添加服务端工具后可能出现的 pause_turn,示例都会抛错。这是一个保守实现,避免把意外停止当成任务成功。
另外,turn < 10 只限制请求轮数,不限制总耗时或总费用。max_tokens: 16000 也不能替代完整的预算控制。
代码没有传 temperature。原文称 API version 2026-10-21 会拒绝它,以及 top_p、top_k,且 TypeScript SDK 默认发送这个版本。这些是作者给出的兼容性说明;其验证范围是 @anthropic-ai/sdk 1.1.0 下的 strict 类型检查和本地 mock,没有真实 API 运行结果。
接入 MCP,不会自动补齐权限和沙箱
工具可以在不同地方执行。客户端工具运行在应用环境里,上面的时钟就是一个;服务端工具运行在提供商基础设施上,原文举例是 OpenAI Code Interpreter;MCP 工具则由 MCP server 执行。
MCP server 可以提供 tools、resources 和 prompts。本地服务通常通过子进程使用 stdio,独立运行的服务可以使用 Streamable HTTP,部署在远程或 localhost。宿主应用为每个 server 运行一个 MCP client,由它调用工具并把结果交回循环。
对模型而言,MCP 工具可以长得和其他工具一样。协议、登录和规则仍需要运行层处理。上面的时钟示例完全没有使用 MCP,它依然具备 Agent 循环。
生产环境还要补三类能力:决定每轮给模型看什么的上下文管理,决定哪些调用需要人确认的权限控制,以及限制命令访问范围的沙箱。作者提到 Claude Agent SDK 会在接近上下文限制时自动压缩旧历史,也讲到自己的编程 Agent 曾未经询问打开公共隧道。后者是个人经历,说明审批为何有用,但不是风险发生率的统计证据。
产品界面与运行层也应分开理解。原文把 Claude Code 的终端应用看作界面,把其下的运行时看作 harness;对应 Python 和 TypeScript 的 Claude Agent SDK,其中 TypeScript 包名是 @anthropic-ai/claude-agent-sdk,与示例使用的 Messages API 客户端不同。
原文还引用 OpenAI 在 2026 年 2 月 4 日的表述:Codex CLI、IDE 扩展和 apps 共用 Codex harness,客户端通过 App Server 接入,后者是使用双向 JSON-RPC 的常驻进程。
我的判断
这篇最有用的地方,是把 Agent 拆成了可以检查的程序行为。读写文件为什么失败,测试为什么没执行,任务为什么提前结束,都能沿着工具声明、调用分发、结果回传和停止条件去排查。
作者的「LLM + harness = agent」适合作为教学模型,他也明确说这不是正式定义。至于两个不同 Agent 经常只是同一模型配了不同工具、指令和 MCP servers,这是观察与判断,原文没有提供对照数据,不能据此推断不同产品效果相近。
手写循环适合学习协议、验证小工具;直接做编程助手时,成熟 SDK 已经承担了不少上下文和权限工作。该选哪条路,取决于你愿意维护多少运行逻辑。原文没有性能、成本或任务成功率对比,无法替团队完成这个取舍。
能拿来做什么
- 在 Node 中学习工具调用时,可以先复现
get_time这样的低风险工具,观察请求、工具结果和stop_reason如何形成闭环,再考虑文件操作。 - 排查 AI 编程助手时,可以按四个环节检查日志:工具是否声明、调用是否收到、执行是否获准、结果是否正确回传,并核对调用 ID。
- 给内部助手添加 shell 前,应同时设计命令权限、工作目录、沙箱和审批规则;轮数上限之外,还要单独处理耗时与费用约束。
- 选择 MCP 或 Agent SDK 前,先明确缺的是工具连接能力,还是上下文与执行管理能力,再决定接入哪一层。