第 8 章 · Agent Loop:从对话到自主执行
前三篇的零件——IPC、LLM 流、工具——在本章装配成 Agent 的心脏。
8.1 核心循环:一段伪代码
先看全景(30 行以内):
messages = [system, ...历史, 用户消息]
tools = 注册中心的全部声明
循环(最多 30 次):
检查取消 / 超时
流式调用 LLM(messages + tools):
text chunk → 推送 text_delta 事件
tool_call chunk → 收集
流结束
如果没有 tool_calls:
推送 task_complete,结束 ✅
执行收集到的每个工具(沙箱校验 → registry.execute)
→ 推送 tool_call / tool_result 事件
把 assistant(tool_calls) 和每个 tool 结果追加进 messages
继续下一轮
就这么多。所有 Agent 框架的核心都是这个循环,差异只在工具丰富度与护栏设计。
8.2 精读:agent-core/loop.ts
入口与参数
agent-core/loop.ts(节选)
export interface AgentLoopParams {
taskId: string
userMessage: string
modelConfig: ModelConfig
workspacePath: string
historyMessages: Array<{
role: string
content: string
toolCalls?: ToolCall[]
toolResults?: ToolResult[]
}>
signal?: AbortSignal // 取消信号,贯穿全程
}
export async function start(
params: AgentLoopParams,
onEvent: (event: AgentEvent) => void
): Promise<void> { ... }
onEvent 回调是 loop 与外界唯一的输出通道(主进程把它接到 webContents.send,见第 5 章)。
消息构建:系统提示词
const systemPrompt = buildAgentSystemPrompt(workspacePath)
const messages: LLMMessage[] = [
{ role: 'system', content: systemPrompt },
...historyToLLMMessages(historyMessages),
{ role: 'user', content: userMessage },
]
agent-core/planner.ts 的系统提示词值得精读,它规定了 Agent 的行为策略:
agent-core/planner.ts(节选)
'## 工作方式(自主决策)',
'### 知识问答 / 解释 / 建议',
'如果你已有的知识足以回答,直接给出回答,不要调用任何工具。',
'',
'### 需要信息的任务',
'如果回答依赖工作区文件或网络信息,先调用工具收集,再基于结果回答。',
'工具调用保持克制:只调用确实需要的。',
'',
'### 需要执行的任务',
'1. 涉及 3 个以上文件或步骤较多时,先用 2-3 句话简述你要做什么,然后直接开始',
'2. 每步关键操作后用一句话说明做了什么',
'3. 完成后给出简短总结',
'',
'### 危险操作',
'删除文件、覆盖已有内容等不可逆操作:先说明影响,得到用户同意后再执行。',
好的系统提示词是「行为规范」而不是「人设介绍」
注意这里没有「你是一个乐于助人的助手」,而是四类场景各自的决策规则:什么时候不调工具、什么时候调、批量任务怎么汇报、危险操作怎么确认。这直接影响 token 消耗与用户体验。
历史转换:historyToLLMMessages
本地存储的历史消息要还原成协议格式,关键是带工具调用的历史必须成对还原:
function historyToLLMMessages(history): LLMMessage[] {
const messages: LLMMessage[] = []
for (const h of history) {
if (h.role === 'user' || h.role === 'assistant') {
const msg: LLMMessage = { role: h.role, content: h.content }
// assistant 消息若带 tool_calls,必须还原 —— 否则后面的 tool 消息会成为孤儿
if (h.role === 'assistant' && h.toolCalls?.length) {
msg.tool_calls = h.toolCalls.map((tc) => ({
id: tc.id,
type: 'function' as const,
function: {
name: tc.name,
arguments: JSON.stringify(tc.arguments), // 注意:协议里是字符串
},
}))
}
messages.push(msg)
}
// 每个工具结果都是一条独立的 role: 'tool' 消息
if (h.toolResults?.length) {
for (const tr of h.toolResults) {
messages.push({
role: 'tool',
content: tr.output,
tool_call_id: tr.toolCallId,
})
}
}
}
return messages
}
这里有个协议细节:请求中 tool_calls 的 arguments 是 JSON 字符串(JSON.stringify(tc.arguments)),而流式响应中收到的也是分片字符串——下一段马上看到。