本文基于 OpenCode 源码,深入分析其 Agent 循环机制、提示词拼装策略、LLM 调用流程以及流式响应处理。

1. 项目概览 链接到标题

OpenCode 是一个 AI 驱动的开发工具,采用 monorepo 架构(bun workspaces + turbo),主要使用 TypeScript 编写。核心依赖包括:

  • Effect — 函数式编程框架,用于组合副作用、依赖注入、错误处理
  • AI SDK(Vercel) — 统一的 LLM 调用接口,支持多 provider
  • SolidJS — 前端框架
  • Hono — HTTP 框架
  • Drizzle — 数据库 ORM

核心代码位于 packages/opencode/src/,其中与 Agent 直接相关的模块:

目录/文件职责
agent/agent.tsAgent 定义与配置
session/prompt.ts用户输入处理、Agent 循环、工具注册
session/llm.tsLLM 调用封装
session/processor.ts流式响应事件处理
session/system.tsSystem Prompt 生成
session/instruction.ts指令文件加载(AGENTS.md 等)
session/compaction.ts上下文压缩

2. 用户输入后的完整链路 链接到标题

当用户在对话框输入内容后,完整的处理链路如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
用户输入
SessionPrompt.prompt()                    ← session/prompt.ts:1371
  ├── createUserMessage()                  ← 解析文本、文件、agent 附件
  │     ├── resolvePart()                  ← 每个附件类型分别处理
  │     ├── 保存 User 消息到数据库
  │     └── 保存各 Part(text/file/agent/subtask)
  └── loop()                               ← session/prompt.ts:1630
        └── runLoop()                      ← while(true) 核心循环
              ├── 加载消息历史
              ├── 拼装 system prompt
              ├── 拼装 messages
              ├── 注册 tools
              ├── processor.process()      ← 发送给 LLM,处理流式响应
              │     └── llm.stream()
              │           └── streamText() ← AI SDK
              ├── 模型返回了 tool-call?
              │     ├── Yes → 执行 tool → 继续循环
              │     └── No  → break,结束
              └── 最终返回 assistant 消息

3. 核心 Agent 循环:runLoop 链接到标题

runLoop() 定义在 session/prompt.ts:1400,是整个 Agent 系统的心脏。它是一个 while(true) 循环,每轮迭代执行以下步骤:

3.1 循环步骤详解 链接到标题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
第 1 步:从数据库加载消息历史(过滤掉已压缩的)
第 2 步:判断是否需要退出循环
         │   最后一条 assistant 消息已经 finish 且不是 "tool-calls" → 退出
第 3 步:step++
第 4 步:如果是第 1 步,异步生成 session 标题(不阻塞主循环)
第 5 步:获取 model(resolve provider + model ID)
第 6 步:处理 subtask(子 agent)或 compaction(上下文压缩)如果有的话
第 7 步:创建一条新的 Assistant 消息(空壳,等待 LLM 填充)
第 8 步:创建 Processor(处理 LLM 流式响应的处理器)
第 9 步:resolveTools() — 收集所有可用工具
第 10 步:拼装 system prompt(env + instructions + skills)
第 11 步:handle.process() — 发给 LLM,流式处理响应
第 12 步:根据结果决定 continue / break

3.2 退出循环的判断逻辑 链接到标题

prompt.ts:1440-1448 中的判断:

1
2
3
4
5
6
7
8
if (
  lastAssistant?.finish &&
  !["tool-calls"].includes(lastAssistant.finish) &&
  !hasToolCalls &&
  lastUser.id < lastAssistant.id
) {
  break  // 退出循环
}

也就是说,只有当满足以下所有条件时才退出:

  • 最后一条 assistant 消息有 finish 标记
  • finish 原因不是 "tool-calls"(意味着模型还要继续调用工具)
  • 没有 pending 的 tool-call
  • 用户消息在 assistant 消息之前(确保模型已经响应了用户)

4. System Prompt 的拼装机制 链接到标题

System Prompt 的拼装分两个阶段完成。

4.1 第一阶段:在 runLoop 中收集 链接到标题

session/prompt.ts:1568-1574

1
2
3
4
5
6
7
const [skills, env, instructions, modelMsgs] = yield* Effect.all([
  sys.skills(agent),           // → string | undefined
  sys.environment(model),      // → string[]
  instruction.system(),        // → string[]
  MessageV2.toModelMessagesEffect(msgs, model),
])
const system = [...env, ...instructions, ...(skills ? [skills] : [])]

这三部分并行获取:

4.1.1 env — 环境信息 链接到标题

session/system.ts:48-62 生成,内容示例:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
You are powered by the model named claude-sonnet-4-20250514.
The exact model ID is anthropic/claude-sonnet-4-20250514
Here is some useful information about the environment you are running in:
<env>
  Working directory: /root/codes/myproject
  Workspace root folder: /root/codes/myproject
  Is directory a git repo: yes
  Platform: linux
  Today's date: Thu May 07 2026
</env>

4.1.2 instructions — 指令文件 链接到标题

session/instruction.ts:149-162 生成。查找逻辑:

  1. 全局指令~/.config/opencode/AGENTS.md~/.claude/CLAUDE.md
  2. 项目指令:从当前工作目录向上搜索 AGENTS.mdCLAUDE.mdCONTEXT.md,取第一个匹配
  3. 远程指令:配置中 instructions 字段里的 URL 列表

每个匹配文件生成一个字符串:

1
2
Instructions from: /root/codes/myproject/AGENTS.md
[文件内容]

4.1.3 skills — 技能描述 链接到标题

session/system.ts:65-77 生成,如果 agent 的权限允许 skill 工具:

1
2
3
4
5
6
7
Skills provide specialized instructions and workflows for specific tasks.
Use the skill tool to load a skill when a task matches its description.

Available skills:
- answers: USE FOR AI-grounded answers via OpenAI-compatible /chat/completions...
- bx: USE FOR web search, research, RAG...
- frontend-design: Create distinctive, production-grade frontend interfaces...

4.2 第二阶段:在 LLM.stream 中合并 链接到标题

session/llm.ts:103-115

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const system: string[] = []
system.push(
  [
    ...(input.agent.prompt ? [input.agent.prompt] : SystemPrompt.provider(input.model)),
    ...input.system,       // ← 上面收集的 env + instructions + skills
    ...(input.user.system ? [input.user.system] : []),
  ]
    .filter((x) => x)
    .join("\n"),           // 用 \n 拼成一个完整字符串,作为 system[0]
)

关键点:所有内容被 .join("\n") 合并为一个字符串,放入 system[0]

Provider prompt 的选择逻辑在 session/system.ts:19-33

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
export function provider(model: Provider.Model) {
  if (model.api.id.includes("gpt-4") || model.api.id.includes("o1") || model.api.id.includes("o3"))
    return [PROMPT_BEAST]       // beast.txt
  if (model.api.id.includes("gpt")) {
    if (model.api.id.includes("codex")) return [PROMPT_CODEX]  // codex.txt
    return [PROMPT_GPT]         // gpt.txt
  }
  if (model.api.id.includes("gemini-")) return [PROMPT_GEMINI]  // gemini.txt
  if (model.api.id.includes("claude")) return [PROMPT_ANTHROPIC] // anthropic.txt
  // ... 其他模型
  return [PROMPT_DEFAULT]       // default.txt
}

4.3 最终发给 LLM 的 messages 结构 链接到标题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
[
  {
    role: "system",
    content: "                              // ← system[0],一个大字符串
      [anthropic.txt 全文]                   ← provider prompt
      \n
      You are powered by...                  ← env
      <env>...</env>
      \n
      Instructions from: /path/AGENTS.md     ← instructions
      [AGENTS.md 内容]
      \n
      Skills provide specialized...          ← skills
      - answers: USE FOR...
      - frontend-design: Create...
    "
  },
  { role: "user", parts: [...] },           // ← 用户历史消息
  { role: "assistant", parts: [...] },      // ← 助手历史消息
  ...
]

5. LLM 调用的五种场景 链接到标题

5.1 主 Agent 循环(最核心) 链接到标题

调用位置processor.ts:673llm.stream()llm.ts:336streamText()

传参

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
streamText({
  temperature,        // agent.temperature ?? 根据模型自动计算
  topP,              // agent.topP ?? 根据模型自动计算
  topK,              // 根据模型自动计算
  maxOutputTokens,   // 根据模型限制计算
  tools,             // 内置工具 + MCP 工具(几十个)
  toolChoice,        // 通常 undefined(auto),结构化输出时 "required"
  messages,          // [system 消息, ...历史对话]
  model,             // 包装后的 language model,带 middleware
  headers,           // session affinity、user-agent 等
  providerOptions,   // provider 特有参数(如 Anthropic 的 extended thinking)
  abortSignal,       // 取消控制
  maxRetries: 0,     // 默认不重试(processor 层面有 retry 逻辑)
  experimental_repairToolCall,  // 工具调用修复(大小写问题)
})

特点:有完整的 tools、完整的 system prompt、使用用户选定的大模型。

5.2 标题生成 链接到标题

调用位置session/prompt.ts:169-229

参数特点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
llm.stream({
  agent: titleAgent,           // 专用 title agent,temperature: 0.5
  small: true,                 // 标记为小请求(用 getSmallModel)
  tools: {},                   // 无工具!
  system: [],                  // 无额外 system
  model: smallModel,           // 用小/便宜模型
  messages: [
    { role: "user", content: "Generate a title for this conversation:\n" },
    ...历史消息(只取第一条真实用户消息之前的上下文)
  ],
})

Agent promptagent/prompt/title.txt)要求:

  • 只输出单行标题
  • ≤50 字符
  • 不解释
  • 与用户语言一致

结果处理:从流中收集所有 text-delta,拼接后取第一个非空行,截断到 100 字符作为标题。

5.3 上下文压缩(Compaction) 链接到标题

调用位置session/compaction.ts:445

参数特点

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
processor.process({
  user: userMessage,
  agent: compactionAgent,    // 专用 compaction agent,权限全部 deny
  tools: {},                 // 无工具!
  system: [],                // 无额外 system(agent 自带 prompt)
  messages: [
    ...历史消息(截断版,stripMedia=true,工具输出截断到 2000 字符),
    { role: "user", content: "Create/Update summary..." }
  ],
})

压缩 prompt 模板要求输出固定结构的 Markdown:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
## Goal
## Constraints & Preferences
## Progress
### Done
### In Progress
### Blocked
## Key Decisions
## Next Steps
## Critical Context
## Relevant Files

压缩流程

  1. 保留最近 N 轮对话(tail_turns,默认 2 轮)
  2. 把之前的消息发给 LLM 生成结构化摘要
  3. 摘要作为 assistant 消息保存,之后加载历史时跳过被压缩的部分
  4. 返回 "continue" 则循环继续,附带一条 "Continue if you have next steps" 的用户消息

5.4 自定义 Agent 生成 链接到标题

调用位置agent/agent.ts:331-399

参数特点

1
2
3
4
5
6
7
8
9
generateObject({   // 注意:不是 streamText!用 generateObject
  temperature: 0.3,
  messages: [
    { role: "system", content: generate.txt },
    { role: "user", content: `Create an agent configuration based on: "${description}"` },
  ],
  model,
  schema: z.object({ identifier, whenToUse, systemPrompt }),
})

特点

  • 使用 generateObject 而非 streamText
  • 要求返回符合 JSON schema 的结构化对象
  • LLM 返回 { identifier, whenToUse, systemPrompt },直接解析为对象

5.5 Summary(会话摘要统计) 链接到标题

调用位置session/summary.ts

注意:实际上不调用 LLM。它只做 diff 计算(通过 snapshot 对比),统计 additions/deletions/files 数量。真正的"摘要文本"是 compaction 场景生成的。

五种场景对比 链接到标题

场景调用方式有 tools有 system prompt模型
主循环streamText 流式有(全部)有(完整)用户选定的
标题生成streamText 流式title.txt小模型
上下文压缩streamText 流式compaction.txt当前模型
Agent 生成generateObjectgenerate.txt默认模型
Summary不调用 LLM

6. 流式响应的解析与处理 链接到标题

所有 streamText 调用返回的都是 result.fullStream,一个异步可迭代的事件流。

6.1 事件类型 链接到标题

AI SDK 的 fullStream 会产出以下类型的事件:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 流控制
{ type: "start" }
{ type: "finish" }

// 文本输出
{ type: "text-start" }
{ type: "text-delta", text: string }
{ type: "text-end" }

// 思考链(部分模型如 Claude、o1)
{ type: "reasoning-start", id: string }
{ type: "reasoning-delta", id: string, text: string, providerMetadata?: any }
{ type: "reasoning-end", id: string }

// 工具调用
{ type: "tool-input-start", id: string, toolName: string }
{ type: "tool-input-delta", id: string, text: string }
{ type: "tool-input-end", id: string }
{ type: "tool-call", toolCallId: string, toolName: string, input: any }
{ type: "tool-result", toolCallId: string, output: any }
{ type: "tool-error", toolCallId: string, error: any }

// 步骤控制(多步调用时)
{ type: "start-step" }
{ type: "finish-step", finishReason: string, usage: LanguageModelUsage, providerMetadata?: any }

// 错误
{ type: "error", error: any }

6.2 Processor 的事件处理 链接到标题

session/processor.ts 中的 handleEvent 函数(第 220-583 行)通过 switch-case 处理每种事件:

事件处理逻辑
start设置 session 状态为 busy
text-start创建 TextPart,写入数据库
text-delta追加文本到 currentText,触发 updatePartDelta(前端实时显示)
text-end触发 plugin hook experimental.text.complete,更新 Part
reasoning-start创建 ReasoningPart,记录 providerMetadata(如 Anthropic signature)
reasoning-delta追加思考内容,触发增量更新
reasoning-end最终更新 ReasoningPart
tool-input-start创建 ToolPart,状态为 pending
tool-call更新状态为 running,检查 doom loop(见下文),触发权限检查
tool-result更新状态为 completed,记录输出
tool-error更新状态为 error,如果是权限拒绝则标记 blocked
start-step记录 snapshot(用于后续 diff 计算)
finish-step计算 token 用量和费用,记录 snapshot diff,检查上下文溢出

6.3 Doom Loop 检测 链接到标题

processor.ts:350-376 实现了 doom loop 检测——如果最近 3 次工具调用都是同一个工具、同一参数,就会触发权限询问:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD)  // 3

if (
  recentParts.length === DOOM_LOOP_THRESHOLD &&
  recentParts.every(part =>
    part.type === "tool" &&
    part.tool === value.toolName &&
    JSON.stringify(part.state.input) === JSON.stringify(value.input)
  )
) {
  yield* permission.ask({ permission: "doom_loop", ... })
}

6.4 工具执行的异步模型 链接到标题

AI SDK 的 streamText 在检测到 tool-call 事件后,会自动执行工具的 execute 函数(在同一次流式调用中),然后产出 tool-result 事件。工具执行的结果直接在同一个流中返回,不需要客户端手动发起第二轮请求。

这意味着一次 streamText 调用可以包含多轮工具使用——模型可以在一个流中:输出文本 → 调用工具 A → 获得结果 → 调用工具 B → 获得结果 → 输出最终文本。每个工具调用周期由 start-step / finish-step 包裹。


7. Agent 体系 链接到标题

7.1 内置 Agent 链接到标题

定义在 agent/agent.ts:111-236

Agent模式说明
buildprimary默认 agent,有完整的工具权限
planprimary规划模式,禁止编辑工具,只能写计划文件
generalsubagent通用子 agent,用于并行研究复杂问题
exploresubagent快速搜索 agent,只允许只读工具(grep、glob、read 等)
compactionprimary (hidden)上下文压缩专用,所有工具都 deny
titleprimary (hidden)标题生成专用,所有工具都 deny,temperature: 0.5
summaryprimary (hidden)摘要生成专用,所有工具都 deny

7.2 Agent 配置结构 链接到标题

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
interface AgentInfo {
  name: string
  description?: string
  mode: "subagent" | "primary" | "all"
  native?: boolean
  hidden?: boolean
  topP?: number
  temperature?: number
  color?: string
  permission: PermissionRuleset
  model?: { modelID: string; providerID: string }
  variant?: string
  prompt?: string        // 自定义 system prompt(如 explore agent 有专用 prompt)
  options: Record<string, unknown>
  steps?: number         // 最大循环步数
}

7.3 用户自定义 Agent 链接到标题

用户可以在配置文件中定义自己的 agent,会与内置 agent 合并:

1
2
3
4
for (const [key, value] of Object.entries(cfg.agent ?? {})) {
  if (value.disable) { delete agents[key]; continue }
  // 合并/覆盖配置...
}

7.4 动态 Agent 生成 链接到标题

通过 Agent.generate() 方法(agent.ts:331-399),可以让 LLM 根据用户描述动态生成一个新的 agent 配置,返回 { identifier, whenToUse, systemPrompt }


8. 工具系统 链接到标题

8.1 工具注册 链接到标题

session/prompt.tsresolveTools() 函数(第 368-546 行)中,工具按以下顺序注册:

  1. 内置工具 — 从 ToolRegistry 获取,包括:

    • bash — 执行命令
    • read / edit / write — 文件操作
    • glob / grep — 文件搜索
    • task — 子 agent 调用
    • webfetch / websearch — 网络搜索
    • skill — 技能加载
    • todowrite — 任务追踪
    • 等等
  2. MCP 工具 — 从 MCP(Model Context Protocol)服务获取的外部工具

  3. 结构化输出工具 — 如果用户请求了 JSON schema 输出格式,会注入 StructuredOutput 工具

8.2 权限过滤 链接到标题

工具注册后会经过权限过滤:

1
2
3
4
5
6
7
8
// llm.ts:449-454
function resolveTools(input) {
  const disabled = Permission.disabled(
    Object.keys(input.tools),
    Permission.merge(input.agent.permission, input.permission ?? []),
  )
  return Record.filter(input.tools, (_, k) => input.user.tools?.[k] !== false && !disabled.has(k))
}

每个 agent 有自己的权限规则(permission),定义了哪些工具允许/拒绝/需询问。例如 explore agent 只允许只读工具。

8.3 工具执行上下文 链接到标题

每个工具执行时接收一个 Context 对象:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
type ToolContext = {
  sessionID: string
  abort: AbortSignal
  messageID: string
  callID: string
  agent: string
  messages: MessageV2.WithParts[]
  metadata: (val) => Effect  // 更新工具调用元数据
  ask: (req) => Effect       // 权限询问
  extra: {
    model: Provider.Model
    bypassAgentCheck: boolean
    promptOps: TaskPromptOps
  }
}

9. 循环退出与上下文管理 链接到标题

9.1 退出条件 链接到标题

Agent 循环在以下情况退出:

  1. 正常完成:LLM 返回 finish 且原因不是 "tool-calls" → 正常退出
  2. 权限阻断processor.process() 返回 "stop"(权限被拒绝、用户拒绝等)
  3. 上下文溢出processor.process() 返回 "compact",需要压缩上下文
  4. 达到步数上限:agent 配置了 stepsstep >= maxSteps
  5. 错误:LLM 调用出错、工具执行出错等

9.2 上下文压缩(Compaction) 链接到标题

当 token 用量超过模型的上下文窗口时(overflow.ts 判断),会自动触发压缩:

  1. 保留最近 N 轮(默认 2 轮)对话作为 “tail”
  2. 将之前的消息(“head”)发给 LLM 生成结构化摘要
  3. 摘要替代原始消息存入历史
  4. 后续加载历史时,通过 filterCompactedEffect 跳过被压缩的部分

9.3 工具输出裁剪(Prune) 链接到标题

compaction.ts:300-344 还实现了工具输出裁剪:

  • 从最新消息向前遍历
  • 保留最近约 40,000 token 的工具调用输出
  • 更早的工具调用输出被替换为 [Old tool result content cleared]

10. 总结 链接到标题

OpenCode 的 Agent 系统可以概括为以下核心设计:

架构模式 链接到标题

事件驱动的 Agent 循环while(true) 循环 + 流式事件处理,每轮把 system prompt + 历史 messages + tools 发给 LLM,流式处理响应,遇到 tool-call 就执行工具然后把结果加回消息历史继续循环。

提示词分层 链接到标题

System Prompt 采用分层拼接策略:Provider 基础 prompt → 环境信息 → 指令文件 → 技能描述,全部合并为一条 system 消息。

多场景复用 链接到标题

同一套 llm.stream() + processor 机制服务于 5 种不同的调用场景,通过不同的 agent 配置(权限、prompt、工具)来区分。

权限与安全 链接到标题

每个 agent 有独立的权限规则,工具执行前会进行权限检查,doom loop 检测防止无限循环。

上下文管理 链接到标题

自动的上下文压缩和工具输出裁剪确保长对话不会超出模型窗口。


本文基于 OpenCode 1.14.40 源码分析,关键文件路径均已标注,可直接在源码中对照阅读。