Appearance
手写 Agent Loop
学框架之前,先把 Agent Loop 本身跑明白。
这节课解决什么问题
- 不依赖框架,怎么手写一个完整 Agent 循环
- 不管底层接的是 HTTP、SDK 还是统一封装,Loop 的骨架都长什么样
- 为什么先理解 Loop,后面学框架和 UI 才不会只会拼装
核心内容
- 消息历史的组织方式
- 工具调用请求、执行、结果回填
- 流式与非流式输出怎么接进同一条循环
- 手动控制多轮循环与终止条件
本节产物
- 一个可替换模型接入层的单 Agent 原型
- 一条工具调用和结果回填链路
- 一份 Loop、SDK、框架三者边界清单
默认示例会先用 Anthropic 风格接口讲清楚循环结构;如果你使用兼容 OpenAI 的 API,关键代码也应该能一一映射到对应写法。
Anthropic 版 / OpenAI 版对照
真正需要固定下来的不是某一家 SDK,而是这 4 个动作:
- 准备消息历史
- 发起一次模型请求
- 识别工具调用意图
- 执行工具后,把结果回填进下一轮消息
接口风格会变,但这条循环不会变。
| 环节 | Anthropic 风格 | OpenAI 兼容风格 |
|---|---|---|
| 消息主体 | messages + content block | messages + message/tool_calls |
| 工具定义 | tools[].input_schema | tools[].function.parameters |
| 工具调用结果 | tool_use block | tool_calls |
| 工具回填方式 | tool_result content block | role: tool 消息 |
Anthropic 风格示例
ts
import Anthropic from '@anthropic-ai/sdk'
const anthropic = new Anthropic({
apiKey: process.env.ANTHROPIC_API_KEY,
})
const messages: Anthropic.Messages.MessageParam[] = [
{
role: 'user',
content: '帮我查询北京天气',
},
]
const response = await anthropic.messages.create({
model: 'claude-sonnet-4-5',
max_tokens: 1024,
messages,
tools: [
{
name: 'get_weather',
description: '查询城市天气',
input_schema: {
type: 'object',
properties: {
city: { type: 'string' },
},
required: ['city'],
},
},
],
})
const toolUse = response.content.find((block) => block.type === 'tool_use')
if (toolUse && toolUse.type === 'tool_use') {
const weather = `晴,22 度,城市:${toolUse.input.city}`
messages.push({
role: 'assistant',
content: response.content,
})
messages.push({
role: 'user',
content: [
{
type: 'tool_result',
tool_use_id: toolUse.id,
content: weather,
},
],
})
}OpenAI 兼容风格示例
ts
import OpenAI from 'openai'
const openai = new OpenAI({
apiKey: process.env.OPENAI_API_KEY,
baseURL: process.env.OPENAI_BASE_URL,
})
const messages: OpenAI.Chat.Completions.ChatCompletionMessageParam[] = [
{
role: 'user',
content: '帮我查询北京天气',
},
]
const response = await openai.chat.completions.create({
model: 'gpt-4.1',
messages,
tools: [
{
type: 'function',
function: {
name: 'get_weather',
description: '查询城市天气',
parameters: {
type: 'object',
properties: {
city: { type: 'string' },
},
required: ['city'],
},
},
},
],
})
const message = response.choices[0]?.message
const toolCall = message?.tool_calls?.[0]
if (message && toolCall) {
const args = JSON.parse(toolCall.function.arguments)
const weather = `晴,22 度,城市:${args.city}`
messages.push(message)
messages.push({
role: 'tool',
tool_call_id: toolCall.id,
content: weather,
})
}你真正该抽象的是什么
如果你想把这节课写成一套可切模型供应商的代码,应该抽象的是这些接口:
sendModelRequest(messages, tools)extractToolCalls(response)appendAssistantTurn(history, response)appendToolResult(history, toolCall, result)
这样底层换成 Anthropic、国产兼容服务,或者兼容 OpenAI 的服务时,主循环都不用重写。
课堂实作
- 用一层最薄的模型接入代码跑通一次多轮工具调用
- 把流式输出拆成展示和执行两个部分
- 记录模型回复、工具调用、工具结果如何进入同一条消息历史
并入项目
这一课会直接作为项目一的第一版实现基础。
面试会怎么问
- 为什么先学手写 Agent Loop,而不是直接上 LangGraph
- Loop、SDK 和框架封装的边界在哪里
