《OpenCode 源码解析》Step 9: User Content 组装和第二次 LLM 调用概览

在工具执行完成后,OpenCode 需要将工具结果组装成模型可理解的消息格式,然后进行第二次 LLM 调用,让 AI 基于工具执行结果生成最终回复。

css 复制代码
┌─────────────────────────────────────────────────────────────────────────────────┐
│                           Step 9: 结果处理与回复生成                              │
├─────────────────────────────────────────────────────────────────────────────────┤
│                                                                                 │
│   Tool 执行完成                                                                  │
│        │                                                                        │
│        ▼                                                                        │
│   ┌──────────────────────┐                                                     │
│   │ 1. Tool Result 保存   │  更新 Part 状态为 completed/error                  │
│   └──────────────────────┘                                                     │
│        │                                                                        │
│        ▼                                                                        │
│   ┌──────────────────────┐                                                     │
│   │ 2. 消息格式转换       │  MessageV2.toModelMessages()                       │
│   │                      │  - 内部格式 → UI 格式 → Model 格式                  │
│   └──────────────────────┘                                                     │
│        │                                                                        │
│        ▼                                                                        │
│   ┌──────────────────────┐                                                     │
│   │ 3. 第二次 LLM 调用    │  streamText()                                      │
│   │                      │  - 包含 tool results                                 │
│   │                      │  - AI 生成基于结果的回复                             │
│   └──────────────────────┘                                                     │
│        │                                                                        │
│        ▼                                                                        │
│   ┌──────────────────────┐                                                     │
│   │ 4. 循环结束或继续     │  - AI 回复完成 → 退出循环                          │
│   │                      │  - AI 调用更多工具 → 继续循环                       │
│   └──────────────────────┘                                                     │
│                                                                                 │
└─────────────────────────────────────────────────────────────────────────────────┘

1. Tool Result 保存

工具执行完成后,结果会被保存到数据库中:

dart 复制代码
// packages/opencode/src/session/prompt.ts 第 469-485 行

if (result && part.state.status === "running") {
  await Session.updatePart({
    ...part,
    state: {
      status: "completed",           // 状态更新为完成
      input: part.state.input,       // 工具输入参数
      title: result.title,           // 工具执行标题
      metadata: result.metadata,     // 元数据(进度信息等)
      output: result.output,         // 工具输出内容
      attachments,                   // 附件(图片、文件等)
      time: {
        ...part.state.time,
        end: Date.now(),             // 记录结束时间
      },
    },
  })
}

错误处理

如果工具执行失败:

css 复制代码
// packages/opencode/src/session/prompt.ts 第 487-500 行

if (!result) {
  await Session.updatePart({
    ...part,
    state: {
      status: "error",               // 状态更新为错误
      error: executionError ? `Tool execution failed: ${executionError.message}` 
                           : "Tool execution failed",
      time: {
        start: part.state.status === "running" ? part.state.time.start : Date.now(),
        end: Date.now(),
      },
      input: part.state.input,
    },
  })
}

2. 消息格式转换

2.1 三层消息格式

OpenCode 使用三种消息表示:

层级

类型

用途

Internal

MessageV2.WithParts

数据库存储格式

UI

UIMessage

界面展示格式

Model

ModelMessage

LLM API 格式

2.2 转换流程

typescript 复制代码
// packages/opencode/src/session/message-v2.ts 第 497-730 行

export function toModelMessages(
  input: WithParts[],      // 内部格式消息
  model: Provider.Model,   // 模型配置
  options?: { stripMedia?: boolean }
): ModelMessage[] {
  
  // Step 1: Internal → UI
  const uiMessages: UIMessage[] = []
  
  for (const msg of input) {
    if (msg.info.role === "user") {
      // 转换用户消息...
    }
    
    if (msg.info.role === "assistant") {
      // 转换助手消息...
      for (const part of msg.parts) {
        if (part.type === "tool") {
          // 转换 tool result
          if (part.state.status === "completed") {
            assistantMessage.parts.push({
              type: ("tool-" + part.tool) as `tool-${string}`,
              state: "output-available",
              toolCallId: part.callID,
              input: part.state.input,
              output: {
                text: outputText,
                attachments: finalAttachments,
              },
            })
          }
          
          if (part.state.status === "error") {
            assistantMessage.parts.push({
              type: ("tool-" + part.tool) as `tool-${string}`,
              state: "output-error",
              toolCallId: part.callID,
              input: part.state.input,
              errorText: part.state.error,
            })
          }
        }
      }
    }
  }
  
  // Step 2: UI → Model (通过 ai SDK 的 convertToModelMessages)
  return convertToModelMessages(uiMessages, { tools })
}

2.3 Tool Result 的特殊处理

媒体文件处理

某些 Provider(如 OpenAI)不支持在 tool result 中直接包含图片/PDF,需要特殊处理:

php 复制代码
// packages/opencode/src/session/message-v2.ts 第 513-523 行

const supportsMediaInToolResults = (() => {
  if (model.api.npm === "@ai-sdk/anthropic") return true
  if (model.api.npm === "@ai-sdk/openai") return true
  if (model.api.npm === "@ai-sdk/amazon-bedrock") return true
  if (model.api.npm === "@ai-sdk/google-vertex/anthropic") return true
  if (model.api.npm === "@ai-sdk/google") {
    const id = model.api.id.toLowerCase()
    return id.includes("gemini-3") && !id.includes("gemini-2")
  }
  return false
})()

// 如果不支持,将媒体文件提取为单独的 user message
if (!supportsMediaInToolResults && mediaAttachments.length > 0) {
  result.push({
    id: MessageID.ascending(),
    role: "user",
    parts: [
      { type: "text", text: "Attached image(s) from tool result:" },
      ...media.map((attachment) => ({
        type: "file",
        url: attachment.url,
        mediaType: attachment.mime,
      })),
    ],
  })
}

Compaction 标记处理

被 compaction 清理的旧 tool result:

dart 复制代码
// packages/opencode/src/session/message-v2.ts 第 638 行

const outputText = part.state.time.compacted 
  ? "[Old tool result content cleared]"  // 被清理的内容显示占位符
  : part.state.output                     // 正常内容

2.4 最终的消息结构

swift 复制代码
{
  "messages": [
    { "role": "system", "content": "..." },
    { "role": "user", "content": "读取 README.md" },
    { 
      "role": "assistant", 
      "content": null,
      "tool_calls": [
        {
          "id": "call_xxx",
          "type": "function",
          "function": {
            "name": "read",
            "arguments": "{\"filePath\": \"README.md\"}"
          }
        }
      ]
    },
    {
      "role": "tool",
      "tool_call_id": "call_xxx",
      "content": "# OpenCode\n\nOpenCode is an AI coding agent..."
    }
  ]
}

3. 第二次 LLM 调用

3.1 调用位置

javascript 复制代码
// packages/opencode/src/session/prompt.ts 第 665-685 行

const result = await processor.process({
  user: lastUser,
  agent,
  abort,
  sessionID,
  system,                                    // System Prompt
  messages: [
    ...MessageV2.toModelMessages(msgs, model),  // 转换后的消息历史
    ...(isLastStep ? [{ role: "assistant", content: MAX_STEPS }] : []),
  ],
  tools,                                     // 可用工具列表
  model,
  toolChoice: format.type === "json_schema" ? "required" : undefined,
})

3.2 与第一次调用的区别

方面

第一次调用

第二次调用

触发时机

用户输入后

Tool 执行完成后

消息内容

用户原始输入

用户输入 + Tool Results

AI 行为

分析需求,选择工具

基于结果生成回复

预期输出

Tool Calls

文本回复或更多 Tool Calls

3.3 可能的 AI 响应

场景 A:直接回复(完成)

ini 复制代码
AI: "根据 README.md 的内容,这是一个 AI 编码助手项目,主要功能包括..."

结果:finish = "stop",退出循环

场景 B:调用更多工具(继续)

css 复制代码
AI: 调用 read({ filePath: "package.json" })

结果:finish = "tool-calls",继续循环,执行工具

场景 C:结构化输出模式

javascript 复制代码
// packages/opencode/src/session/prompt.ts 第 689-708 行

// 如果配置了 JSON Schema 输出格式
if (lastUser.format?.type === "json_schema") {
  // 注入 StructuredOutput 工具
  tools["StructuredOutput"] = createStructuredOutputTool({
    schema: lastUser.format.schema,
    onSuccess(output) {
      structuredOutput = output
    },
  })
}

// AI 必须调用 StructuredOutput 工具
if (structuredOutput !== undefined) {
  processor.message.structured = structuredOutput
  processor.message.finish = "stop"
  await Session.updateMessage(processor.message)
  break  // 退出循环
}

// 如果 AI 没有调用 StructuredOutput 工具
if (modelFinished && format.type === "json_schema") {
  processor.message.error = new MessageV2.StructuredOutputError({
    message: "Model did not produce structured output",
    retries: 0,
  }).toObject()
  break
}

4. 循环控制与结束

4.1 循环继续的条件

php 复制代码
// packages/opencode/src/session/prompt.ts 第 711-721 行

if (result === "stop") break  // 正常结束

if (result === "compact") {
  // 需要 compaction,创建 compaction 任务后继续
  await SessionCompaction.create({
    sessionID,
    agent: lastUser.agent,
    model: lastUser.model,
    auto: true,
    overflow: !processor.message.finish,
  })
}
continue  // 继续循环

4.2 结束后的清理

scss 复制代码
// packages/opencode/src/session/prompt.ts 第 723-732 行

SessionCompaction.prune({ sessionID })  // 轻量清理

// 通知等待的回调
for await (const item of MessageV2.stream(sessionID)) {
  if (item.info.role === "user") continue
  const queued = state()[sessionID]?.callbacks ?? []
  for (const q of queued) {
    q.resolve(item)  // 返回最终消息
  }
  return item
}

5. 完整流程图

yaml 复制代码
┌─────────────────────────────────────────────────────────────────────────────┐
│                         Step 9 完整流程                                      │
└─────────────────────────────────────────────────────────────────────────────┘

  Tool 执行完成
       │
       ▼
  ┌─────────────────────────────────────┐
  │ 保存 Tool Result 到数据库            │
  │ - status: "completed"/"error"       │
  │ - output: 工具输出内容               │
  │ - attachments: 附件列表              │
  └─────────────────────────────────────┘
       │
       ▼
  ┌─────────────────────────────────────┐
  │ 消息格式转换 (toModelMessages)       │
  │                                     │
  │ Internal (WithParts)                │
  │      │                              │
  │      ▼                              │
  │    UI (UIMessage)                   │
  │      │                              │
  │      ▼                              │
  │   Model (ModelMessage)              │
  │      │                              │
  │      └── 特殊处理:                  │
  │          - 媒体文件提取              │
  │          - Compaction 占位符         │
  │          - 错误状态标记              │
  └─────────────────────────────────────┘
       │
       ▼
  ┌─────────────────────────────────────┐
  │ 组装第二次 LLM 调用参数              │
  │ - system: System Prompt             │
  │ - messages: 完整消息历史             │
  │ - tools: 可用工具列表                │
  └─────────────────────────────────────┘
       │
       ▼
  ┌─────────────────────────────────────┐
  │ 第二次 LLM 调用 (streamText)         │
  │                                     │
  │ AI 看到:用户问题 + Tool Results     │
  └─────────────────────────────────────┘
       │
       ├── AI 直接回复 ─────────────────┐
       │                                 ▼
       │                          ┌─────────────┐
       │                          │ finish=stop │
       │                          └──────┬──────┘
       │                                 │
       ▼                                 ▼
  ┌─────────────────┐           ┌──────────────┐
  │ AI 调用更多工具 │           │ 循环结束     │
  │                 │           │              │
  │ finish=tool-    │           │ 返回最终消息 │
  │ calls           │           │ 给调用方     │
  └────────┬────────┘           └──────────────┘
           │
           ▼
    回到 Step 8 (Tool 执行)

6. 关键设计要点

6.1 为什么需要第二次 LLM 调用?

设计

说明

分离关注点

第一次调用决定"做什么",第二次调用决定"说什么"

Tool Results 上下文

AI 需要看到 tool results 才能给出准确回复

迭代处理

支持多轮工具调用直到任务完成

6.2 消息格式的兼容性

sql 复制代码
不同 Provider 的差异处理:
├── Anthropic: 支持 tool result 中的图片
├── OpenAI: 支持 tool result 中的图片  
├── Google: 部分支持(仅 Gemini-3)
└── 其他: 需要将媒体提取为单独的 user message

6.3 Compaction 对消息的影响

diff 复制代码
被 Compaction 的 Tool Result:
- 原始内容被替换为 "[Old tool result content cleared]"
- 保留 tool 调用记录(用于对话连贯性)
- 节省上下文空间

7. 相关代码文件

文件

作用

packages/opencode/src/session/prompt.ts

主循环,第二次 LLM 调用

packages/opencode/src/session/message-v2.ts

消息格式转换 toModelMessages()

packages/opencode/src/session/processor.ts

SessionProcessor.process() 封装 LLM 调用

8. 总结

Step 9 的核心任务:

  1. 保存结果:将 tool execution 结果持久化到数据库
  2. 格式转换:将内部消息格式转换为 LLM API 格式
  3. 第二次调用:让 AI 基于 tool results 生成回复
  4. 循环控制:决定是结束对话还是继续执行更多工具

这是 OpenCode 对话流程的最后一个关键步骤,完成了从"执行"到"回复"的闭环。

相关推荐
明君879972 小时前
Genkit Google 开源的 AI 应用开发框架介绍
人工智能
AI攻城狮2 小时前
长上下文不是长期记忆:为什么 1M Context 也不会淘汰 RAG
人工智能·云原生·aigc
疲惫的神熊猫2 小时前
Linux(Ubuntu)部署Ollama+Qwen(千问)本地大模型实战01
人工智能
码路飞2 小时前
熬夜看完 GTC 2026 Keynote,这 5 个发布跟开发者最相关(不只是显卡)
人工智能
数字生命卡兹克2 小时前
AI,正在吞噬所有软件。
人工智能
深视智能科技2 小时前
AIR 系列:极端环境下的高速成像方案
人工智能·科技·相机
HyperAI超神经3 小时前
基于2.5万临床数据,斯坦福大学发布首个原生3D腹部CT视觉语言模型,Merlin在752类任务中全面领先
人工智能·深度学习·神经网络·机器学习·3d·语言模型·cpu
飞Link3 小时前
具身智能:从大脑到四肢,AI跨越物理世界的全景指南
人工智能·机器人
KKKlucifer3 小时前
数据资产化背景下的分类分级技术架构设计
人工智能·安全·重构