Skip to content

手写 Agent Loop

学框架之前,先把 Agent Loop 本身跑明白。


这节课解决什么问题

  • 不依赖框架,怎么手写一个完整 Agent 循环
  • 不管底层接的是 HTTP、SDK 还是统一封装,Loop 的骨架都长什么样
  • 为什么先理解 Loop,后面学框架和 UI 才不会只会拼装

核心内容

  • 消息历史的组织方式
  • 工具调用请求、执行、结果回填
  • 流式与非流式输出怎么接进同一条循环
  • 手动控制多轮循环与终止条件

本节产物

  • 一个可替换模型接入层的单 Agent 原型
  • 一条工具调用和结果回填链路
  • 一份 Loop、SDK、框架三者边界清单

默认示例会先用 Anthropic 风格接口讲清楚循环结构;如果你使用兼容 OpenAI 的 API,关键代码也应该能一一映射到对应写法。

Anthropic 版 / OpenAI 版对照

真正需要固定下来的不是某一家 SDK,而是这 4 个动作:

  1. 准备消息历史
  2. 发起一次模型请求
  3. 识别工具调用意图
  4. 执行工具后,把结果回填进下一轮消息

接口风格会变,但这条循环不会变。

环节Anthropic 风格OpenAI 兼容风格
消息主体messages + content blockmessages + message/tool_calls
工具定义tools[].input_schematools[].function.parameters
工具调用结果tool_use blocktool_calls
工具回填方式tool_result content blockrole: 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 和框架封装的边界在哪里

大齐 AI 课堂 · 程序员的 Agent 开发课