OpenCode提示词处理流程

OpenCode提示词处理流程

当前版本1.2.6。

0. 资料

  1. OpenCode Compaction 上下文压缩 和 清理旧工具输出
  2. OpenCode Subtask 子任务处理
  3. OpenCode 调用大模型
  4. OpenCode Agent
  5. OpenCode 构建提示词
  6. OpenCode 工具系统
  7. OpenCode 服务端流式响应 和 用户确认机制
  8. OpenCode LSP

1. 整体架构概览

1.1 代码位置

客户端:web端,代码在packages/app目录下。

服务端:代码在packages/opencode目录下。

1.2 客户端提示词组件代码位置和提交链路

app.tsx
    ↓
pages/session.tsx Session组件
    ↓
pages/session/session-prompt-dock.tsx SessionPromptDock组件
    ↓
components/prompt-input.tsx PromptInput组件
    ↓
components/prompt-input/submit.ts handleSubmit函数
	- send
		- client.session.promptAsync url: /session/{sessionID}/prompt_async

/session/{sessionID}/prompt_async会将用户输入的提示词发送给服务端然后立即返回。

服务端会处理的流式过程、结果会通过context/global-sync.tsx中的下列代码监听:

const unsub = globalSDK.event.listen((e) => {
  const directory = e.name
  const event = e.details

  if ((event.type as string) !== "server.heartbeat") {
    // Do not print heartbeat
    log.info(`[event][listen] directory=${directory}, event=${JSON.stringify(event)}`)
  }

  if (directory === "global") {
    applyGlobalEvent({
      event,
      project: globalStore.project,
      refresh: queue.refresh,
      setGlobalProject(next) {
        if (typeof next === "function") {
          setGlobalStore("project", produce(next))
          return
        }
        setGlobalStore("project", next)
      },
    })
    if (event.type === "server.connected" || event.type === "global.disposed") {
      for (const directory of Object.keys(children.children)) {
        queue.push(directory)
      }
    }
    return
  }

  const existing = children.children[directory]
  if (!existing) return
  children.mark(directory)
  const [store, setStore] = existing
  applyDirectoryEvent({
    event,
    directory,
    store,
    setStore,
    push: queue.push,
    setSessionTodo,
    vcsCache: children.vcsCache.get(directory),
    loadLsp: () => {
      sdkFor(directory)
        .lsp.status()
        .then((x) => setStore("lsp", x.data ?? []))
    },
  })
})

1.3 调用链路概览

┌─────────────────────────────────────────────────────────────────────┐
│ 客户端                                                               │
├─────────────────────────────────────────────────────────────────────┤
components/prompt-input/submit.ts handleSubmit函数
	- send
		- client.session.promptAsync url: /session/{sessionID}/prompt_async
    ↓
┌─────────────────────────────────────────────────────────────────────┐
│ 服务端                                                               │
├─────────────────────────────────────────────────────────────────────┤
入口点:server/server.ts SessionRoutes -> server/routes/session.ts /:sessionID/prompt_async
    ↓
SessionPrompt.prompt					# 提示词处理 session/prompt.ts
    ↓
SessionPrompt.loop()         	# 主循环,处理提示词到模型的交互 session/prompt.ts
    ↓
    ├─ resume_existing? ──是──► resume(sessionID) ──┐
    │                                               │
    └─ resume_existing? ──否──► start(sessionID)  ──┴──► abort
                                                          │
                    ┌─────────────────────────────────────┘
                    ▼
            ┌───────────────────┐
            │  while (true)     │ ◄─────────────────────────────┐
            └───────────────────┘                               │
                    │                                           │
                    ▼                                           │
            读取消息历史 + 倒序遍历                                │
                    │                                           │
                    ▼                                           │
            ┌───────────────────┐                               │
            │ finish 检查        │──是──► break ──► 结束阶段      │
            └───────────────────┘                               │
                    │ 否                                        │
                    ▼                                           │
            ┌───────────────────┐                               │
            │ subtask?          │──是──► 执行 ──► continue ──────┤
            └───────────────────┘                               │
                    │ 否                                        │
                    ▼                                           │
            ┌───────────────────┐                               │
            │ compaction?       │──是──► 执行 ──► continue ──────┤
            └───────────────────┘                               │
                    │ 否                                        │
                    ▼                                           │
            processor.process()                                 │
     流式响应 → 数据库 → Bus 事件 → 前端展示                        │
                    │                                           │
                    ├─ stop   ──► break ──► 结束阶段             │
                    ├─ compact ──► 压缩 ──► continue ────────────┤
                    └─ 其他   ──► continue ──────────────────────┘

1.4 核心文件位置

模块 文件路径
提示词处理入口 src/session/prompt.ts
消息数据结构 src/session/message-v2.ts
处理器 src/session/processor.ts
大模型调用 src/session/llm.ts
上下文压缩 src/session/compaction.ts
系统提示词 src/session/system.ts
指令提示词 src/session/instruction.ts
Session 管理 src/session/index.ts
Agent 配置 src/agent/agent.ts
工具注册 src/tool/registry.ts
权限管理 src/permission/next.ts

2. 数据模型:Session、Message 和 Part 的关系

理解 OpenCode 的数据处理流程,首先需要理解其核心数据模型。OpenCode 使用三层结构来组织对话数据:Session → Message → Part。

2.1 数据库表结构

位置: src/session/session.sql.ts

-- Session 表:一次完整的对话会话
SessionTable:
  id (PK)
  project_id (FK)
  title
  permission (JSON)
  time_created, time_updated, ...

-- Message 表:一次对话轮次(用户或助手)
MessageTable:
  id (PK)
  session_id (FK → SessionTable.id, CASCADE DELETE)
  time_created
  data (JSON: role, agent, model, tokens, finish, ...)

-- Part 表:消息内的具体内容单元
PartTable:
  id (PK)
  message_id (FK → MessageTable.id, CASCADE DELETE)
  session_id (冗余字段,用于快速查询)
  time_created
  data (JSON: type, text, tool, state, ...)

2.2 层级关系图

                    Session (会话)
                    一个完整的对话会话
                         │
                         │ 1:N
                         ▼
    ┌──────────────────────────────────────┐
    │              Message                  │
    │       (一次对话轮次)                   │
    │                                       │
    │  角色: "user" | "assistant"           │
    │  元数据: agent, model, tokens, finish │
    │  关联: parentID (助手消息关联用户消息)  │
    └──────────────────────────────────────┘
                         │
                         │ 1:N
                         ▼
    ┌──────────────────────────────────────┐
    │                Part                   │
    │        (消息内的内容单元)              │
    │                                       │
    │  类型: text | tool | reasoning | ...  │
    │  内容: 由 type 决定具体字段            │
    │  关联: messageID (所属消息)            │
    └──────────────────────────────────────┘

2.3 Message(消息)详解

Message 代表对话中的一次交互轮次,分为两种角色:

User Message(用户消息)

{
  id: "message_abc123",
  sessionID: "session_xyz",
  role: "user",
  agent: "build",                    // 使用的 agent
  model: { providerID: "anthropic", modelID: "claude-sonnet-4-20250514" },
  time: { created: 1700000000000 }
}

Assistant Message(助手消息)

{
  id: "message_def456",
  sessionID: "session_xyz",
  role: "assistant",
  parentID: "message_abc123",        // 关联到触发它的用户消息
  agent: "build",
  modelID: "claude-sonnet-4-20250514",
  providerID: "anthropic",
  tokens: { input: 100, output: 200, ... },
  finish: "stop",                    // 完成原因
  time: { created: 1700000001000, completed: 1700000005000 }
}

2.4 Part(消息部分)详解

Part 代表一条消息内的具体内容单元,一条消息可以包含多个 part:

type Part =
  | TextPart        // 文本内容
  | ToolPart        // 工具调用
  | ReasoningPart   // 推理过程(如 Claude 的思考)
  | FilePart        // 文件附件
  | SubtaskPart     // 子任务
  | CompactionPart  // 压缩标记
  | StepStartPart   // 步骤开始标记
  | StepFinishPart  // 步骤结束标记(含 token 统计)
  | ...

2.5 实际示例

假设用户发送 "读取 package.json 文件并解释它",完整的消息和 part 结构如下:

┌─────────────────────────────────────────────────────────────────────┐
│ Session: session_xyz                                                │
├─────────────────────────────────────────────────────────────────────┤
│                                                                     │
│  Message 1 (User) - id: msg_001                                     │
│  ├── Part 1: TextPart                                               │
│  │   └── text: "读取 package.json 文件并解释它"                       │
│  └── Part 2: FilePart (如果用户拖拽了文件)                            │
│      └── url: "file:///path/to/package.json"                        │
│                                                                     │
│  Message 2 (Assistant) - id: msg_002, parentID: msg_001             │
│  ├── Part 1: StepStartPart                                          │
│  │   └── 标记步骤开始                                                │
│  ├── Part 2: TextPart                                               │
│  │   └── text: "我来读取这个文件..."                                 │
│  ├── Part 3: ToolPart                                               │
│  │   ├── tool: "read"                                               │
│  │   ├── callID: "call_abc123"                                      │
│  │   └── state: { status: "completed", output: "{ ... }" }          │
│  ├── Part 4: TextPart                                               │
│  │   └── text: "这是 package.json 的解释..."                         │
│  └── Part 5: StepFinishPart                                         │
│      ├── reason: "stop"                                             │
│      └── tokens: { input: 500, output: 300 }                        │
│                                                                     │
└─────────────────────────────────────────────────────────────────────┘

2.6 设计原因

Message 的作用

  • 对话层级: 区分用户和助手的交互轮次
  • 元数据存储: 模型信息、token 使用量、完成状态等
  • 关联追踪: 通过 parentID 知道哪个助手消息响应用户消息

Part 的作用

  • 细粒度更新: 流式返回时可以逐个 part 更新,前端实时显示
  • 类型区分: 不同类型的内容有不同的处理方式
  • 工具追踪: 每个 tool 调用是独立的 part,可以追踪状态
  • 压缩支持: 可以单独标记旧 part 为已压缩,减少上下文

2.7 代码中的使用

// 创建用户消息
const userMessage = await Session.updateMessage({
  id: Identifier.ascending("message"),
  sessionID,
  role: "user",
  agent: "build",
  model: { providerID, modelID },
})

// 添加文本 part
await Session.updatePart({
  id: Identifier.ascending("part"),
  messageID: userMessage.id,    // 关联到消息
  sessionID,
  type: "text",
  text: "用户输入的内容",
})

// 创建助手消息
const assistantMessage = await Session.updateMessage({
  id: Identifier.ascending("message"),
  parentID: userMessage.id,     // 关联到用户消息
  sessionID,
  role: "assistant",
  // ...
})

// 流式更新文本 part
await Session.updatePartDelta({
  sessionID,
  messageID: assistantMessage.id,
  partID: textPartID,
  field: "text",
  delta: "新增的文本片段",
})

updateMessage 和 updatePart 的 Upsert 机制

这两个函数虽然命名为 update,但实际是 "创建或更新"(Upsert) 操作:

位置: src/session/index.ts:581-601 和 src/session/index.ts:646-666

// 核心实现:INSERT ... ON CONFLICT DO UPDATE
db.insert(MessageTable)
  .values({...})
  .onConflictDoUpdate({ target: MessageTable.id, set: { data } })
  .run()
情况 行为
id 不存在 插入新记录(创建)
id 已存在 更新现有记录

返回值:两个函数都返回传入的数据本身(return msg / return part)。

设计优点:

  1. 统一接口:不需要区分 create 和 update,简化调用方逻辑
  2. 幂等性:多次调用同一个 id 不会产生重复数据
  3. 流式更新友好:先创建消息框架,后续再更新内容(如添加 finish 状态)

使用示例:

// 1. 创建新消息
const msg = await Session.updateMessage({
  id: Identifier.ascending("message"),  // 新 id
  sessionID,
  role: "user",
})

// 2. 更新已有消息(使用同一个 id)
await Session.updateMessage({
  ...msg,  // 复用原消息的 id 和其他字段
  finish: "stop",
  time: { ...msg.time, completed: Date.now() }
})

2.8 查询完整消息

// 获取消息及其所有 parts
export const parts = fn(Identifier.schema("message"), async (message_id) => {
  const rows = Database.use((db) =>
    db.select().from(PartTable)
      .where(eq(PartTable.message_id, message_id))
      .orderBy(PartTable.id)
      .all(),
  )
  return rows.map((row) => ({
    ...row.data,
    id: row.id,
    sessionID: row.session_id,
    messageID: row.message_id,
  }))
})

// 完整的消息结构
type WithParts = {
  info: MessageV2.Info     // 消息元数据
  parts: MessageV2.Part[]  // 该消息的所有 parts
}

2.9 总结

概念 粒度 作用 关键字段
Session 会话级别 一次完整的对话会话 id, title, permission
Message 轮次级别 用户或助手的一次交互 id, role, parentID, tokens
Part 内容单元 消息内的具体内容 id, messageID, type

关系: Session → 1:N → Message → 1:N → Part

3. [服务端] SessionPrompt.loop提示词循环处理函数执行流程图

代码在session/prompt.ts的loop函数中。

3.1 loop函数执行流程图

┌─────────────────────────────────────────────────────────────────────────────┐
│                              【初始化阶段】                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│    loop(sessionID, resume_existing)                                         │
│                    │                                                        │
│                    ▼                                                        │
│         ┌─────────────────────┐                                             │
│         │  resume_existing ?  │                                             │
│         └─────────────────────┘                                             │
│              │           │                                                  │
│         是   │           │  否                                              │
│              ▼           ▼                                                  │
│        resume()     start()                                                 │
│              │           │                                                  │
│              └─────┬─────┘                                                  │
│                    ▼                                                        │
│           abort controller                                                   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
                                     │
                                     ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                           【主循环 while(true)】                              │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│    ┌──────────────────────────────────────────────────────────────────┐     │
│    │  ① 读取消息历史: MessageV2.filterCompacted(stream)               │     │
│    │  ② 倒序遍历查找: lastUser, lastAssistant, lastFinished, tasks    │     │
│    └──────────────────────────────────────────────────────────────────┘     │
│                                     │                                       │
│                                     ▼                                       │
│         ┌─────────────────────────────────────────────┐                     │
│         │ lastAssistant.finish 存在                    │                     │
│         │ 且 非 tool-calls/unknown ?                   │                     │
│         └─────────────────────────────────────────────┘                     │
│                      │                    │                                 │
│                 是   │                    │  否                             │
│                      ▼                    ▼                                 │
│              ┌──────────────┐    ┌─────────────────────┐                    │
│              │ break 退出    │    │ task.type ===       │                    │
│              │  (→结束阶段)  │    │   'subtask' ?       │                    │
│              └──────────────┘    └─────────────────────┘                    │
│                                     │           │                           │
│                                是   │           │  否                       │
│                                     ▼           ▼                           │
│                          ┌──────────────┐  ┌─────────────────────┐          │
│                          │ Subtask.     │  │ task.type ===       │          │
│                          │  process()   │  │   'compaction' ?    │          │
│                          └──────────────┘  └─────────────────────┘          │
│                                     │           │           │               │
│                                     │      是   │           │  否           │
│                                     │           ▼           ▼               │
│                                     │  ┌──────────────┐  ┌────────────┐    │
│                                     │  │ Compaction.  │  │ 正常处理    │    │
│                                     │  │  process()   │  │ 流程       │    │
│                                     │  └──────────────┘  └────────────┘    │
│                                     │           │               │           │
│                                     │           │               ▼           │
│                                     │           │    ┌──────────────────┐  │
│                                     │           │    │ 获取 Agent       │  │
│                                     │           │    │ 创建 Processor   │  │
│                                     │           │    │ processor.process│  │
│                                     │           │    └──────────────────┘  │
│                                     │           │               │           │
│                                     │           │               ▼           │
│                                     │           │    ┌──────────────────┐  │
│                                     │           │    │   处理结果       │  │
│                                     │           │    └──────────────────┘  │
│                                     │           │        │    │    │       │
│                                     │           │   stop │    │    │其他   │
│                                     │           │        │ compact   │     │
│                                     │           │        ▼    ▼    ▼       │
│                                     │           │     (退出) (压缩)(继续)  │
│                                     │           │           │              │
│                                     │           │           ▼              │
│                                     ▼           ▼        continue          │
│                              ◄─────────────────────────────┘                │
│                              │                                              │
│                        continue                                             │
│                              │                                              │
│                              └───────────────────────────► (返回循环开始)   │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘
                                     │
                                     ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                               【结束阶段】                                   │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│           构建最终消息 finalMessage                                          │
│                    │                                                        │
│                    ▼                                                        │
│           return finalMessage                                               │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

3.2 SessionProcessor.process执行流程图

┌─────────────────────────────────────────────────────────────────────────────┐
│                              【初始化】                                       │
├─────────────────────────────────────────────────────────────────────────────┤
│  process(streamInput)                                                        │
│      │                                                                       │
│      ▼                                                                       │
│  needsCompaction = false                                                     │
│  shouldBreak = 配置项                                                         │
│      │                                                                       │
│      ▼                                                                       │
│  ┌───────────────────────────────────────┐                                   │
│  │         while (true)                  │ ◄─────────────────────────┐      │
│  └───────────────────────────────────────┘                           │      │
│      │                                                               │      │
│      ▼                                                               │      │
│  LLM.stream(streamInput)  ─────────────────► 大模型 API              │      │
│      │                                                               │      │
│      ▼                                                               │      │
│  ┌───────────────────────────────────────┐                           │      │
│  │  for await (value of fullStream)      │ ◄─────────┐               │      │
│  └───────────────────────────────────────┘           │               │      │
│      │                                               │               │      │
│      ▼                                               │               │      │
│  ┌───────────────────────────────────────────────────┤               │      │
│  │              switch (value.type)                  │               │      │
│  └───────────────────────────────────────────────────┤               │      │
│      │                                               │               │      │
│      ├─ start          → 设置状态 busy                │               │      │
│      ├─ reasoning-*    → 处理推理内容                  │               │      │
│      ├─ text-*         → 处理文本内容                  │               │      │
│      ├─ tool-input-*   → 处理工具输入                  │               │      │
│      ├─ tool-call      → 执行工具(检测 doom loop)      │               │      │
│      ├─ tool-result    → 更新工具结果                  │               │      │
│      ├─ tool-error     → 更新错误状态(可能 blocked)     │               │      │
│      ├─ start-step     → 跟踪快照                     │               │      │
│      ├─ finish-step    → 更新 tokens/cost             │               │      │
│      │                    检查 isOverflow             │               │      │
│      ├─ finish         → 流结束                       │               │      │
│      └─ error          → 抛出错误 ────────────────────┼──► 异常处理     │      │
│      │                                               │               │      │
│      ▼                                               │               │      │
│  ┌───────────────────────┐                           │               │      │
│  │  needsCompaction ?    │──是──► break ──────────────┼──► 清理阶段    │      │
│  └───────────────────────┘                           │               │      │
│      │ 否                                             │               │      │
│      └───────────────────────────────────────────────┘               │      │
│                                                                      │      │
│  异常处理:                                                             │      │
│  ┌───────────────────────┐                                           │      │
│  │  可重试错误 ?           │──是──► 等待延迟 ───────────────────────────┘      │
│  └───────────────────────┘                                                │
│      │ 否                                                                 │
│      ▼                                                                    │
│  设置 error → 发布事件 → 清理阶段                                          │
│                                                                           │
└─────────────────────────────────────────────────────────────────────────────┘
                                     │
                                     ▼
┌─────────────────────────────────────────────────────────────────────────────┐
│                              【清理阶段】                                     │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  1. 处理未完成的 tool parts (status 设为 error)                                │
│  2. 更新 message.time.completed                                              │
│  3. Session.updateMessage 保存消息                                            │
│                                                                             │
│  ┌───────────────────────────────────────────────────────────────┐          │
│  │                      返回值判断                                 │          │
│  ├───────────────────────────────────────────────────────────────┤          │
│  │  needsCompaction ? ──是──► return "compact"                   │           │
│  │  blocked ?         ──是──► return "stop"                      │           │
│  │  error ?           ──是──► return "stop"                      │           │
│  │  正常               ──►   return "continue"                    │          │
│  └───────────────────────────────────────────────────────────────┘          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

4. [服务端] 读取消息历史MessageV2.filterCompacted解析

位置: src/session/message-v2.ts

export async function filterCompacted(stream: AsyncIterable<MessageV2.WithParts>) {
  const result = [] as MessageV2.WithParts[]
  const completed = new Set<string>()

  for await (const msg of stream) {
    result.push(msg)

    // 如果是用户消息,且已完成压缩,且包含 compaction part,则停止,因为后续的消息都是压缩后的结果,不需要再处理了
    if (
      msg.info.role === "user" &&
      completed.has(msg.info.id) &&
      msg.parts.some((part) => part.type === "compaction")
    )
      break

    // 如果是助手消息且已总结完成,标记其父消息为已完成,父消息即用户消息
    if (msg.info.role === "assistant" && msg.info.summary && msg.info.finish)
      completed.add(msg.info.parentID)
  }

  // 反转顺序,数据库是倒序查询的,但模型需要按时间正序处理(最早的在前)
  result.reverse()
  return result
}

4.1 为什么要读取历史数据?

  1. 构建上下文: 大模型需要历史对话来理解当前任务
  2. 保持连续性: 维持对话的上下文连贯性
  3. 支持压缩点: 通过 compaction 标记确定有效历史范围

4.2 为什么要 reverse()?

MessageV2.stream() 从数据库按时间倒序读取(最新的在前),但模型需要按时间正序处理(最早的在前),所以需要 reverse()。

5. [服务端] MessageV2.filterCompacted的结果使用解析

返回结果的使用位置:src/session/prompt.ts的loop函数中。

5.1 为什么要倒着遍历 (for let i = msgs.length - 1)?

// 查找最后的用户消息和助手消息
let lastUser: MessageV2.User | undefined
let lastAssistant: MessageV2.Assistant | undefined
let lastFinished: MessageV2.Assistant | undefined
let tasks: (MessageV2.CompactionPart | MessageV2.SubtaskPart)[] = []
// 倒序遍历,可以快速找到最近的消息、收集最近的待处理任务(subtask/compaction)
for (let i = msgs.length - 1; i >= 0; i--) {
  const msg = msgs[i]
  if (!lastUser && msg.info.role === "user") lastUser = msg.info as MessageV2.User
  if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info as MessageV2.Assistant
  if (!lastFinished && msg.info.role === "assistant" && msg.info.finish)
    lastFinished = msg.info as MessageV2.Assistant
  if (lastUser && lastFinished) break

  const task = msg.parts.filter((part) => part.type === "compaction" || part.type === "subtask")
  if (task && !lastFinished) {
    tasks.push(...task)
  }
}

变量含义和后续用途:

变量 含义 后续用途
lastUser 最后一条用户消息 1. 获取 agent、model 配置
2. 作为 processor.process() 的 user 参数
3. 创建 compaction 时需要这些信息
lastAssistant 最后一条助手消息 1. 判断模型是否已完成响应(通过 finish 字段)
2. 退出循环的判断条件
lastFinished 最后一条已完成的助手消息(有 finish 字段) 1. 获取 tokens 统计信息
2. 判断是否触发压缩(isOverflow 需要 tokens)
3. 压缩点有效性判断(summary && finish)
tasks 待处理的 compaction/subtask parts 1. 后续取出最新的 task 进行处理
2. 通过 task.type 判断处理逻辑

5.1.1 为什么要倒着遍历?

原因:

  • 倒序遍历可以快速找到最近的消息
  • 找到 lastUser 和 lastFinished 后立即 break,提高效率
  • 收集最近的待处理任务(subtask/compaction)

5.1.2 为什么需要区分 lastAssistant 和 lastFinished?

// lastAssistant:只要最后一条助手消息
if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info

// lastFinished:需要已完成(有 finish 字段)
if (!lastFinished && msg.info.role === "assistant" && msg.info.finish) lastFinished = msg.info
  • lastAssistant 可能没有 finish(正在生成中、被中断等)
  • lastFinished 一定有 finish,所以有 tokens 统计,可以用于判断是否需要压缩

5.1.3 有 lastAssistant 但没有 lastFinished 的情况

什么情况下会发生?

情况 说明
正在生成中 助手消息刚开始创建,还没有完成,finish 字段尚未设置
被中断 用户取消了请求,助手消息没有正常完成
出错 API 调用失败,没有生成完整的响应

代码判断逻辑:

// lastAssistant:只要有助手消息就设置
if (!lastAssistant && msg.info.role === "assistant") lastAssistant = msg.info

// lastFinished:需要有 finish 字段才设置
if (!lastFinished && msg.info.role === "assistant" && msg.info.finish)
  lastFinished = msg.info

这种情况下的处理:

// 1. 退出循环检查 - lastAssistant.finish 不存在,不会退出
if (lastAssistant?.finish && ...) {
  break  // 不进入,继续循环
}

// 2. 自动压缩检查 - lastFinished 不存在,不会触发
if (lastFinished && lastFinished.summary !== true && isOverflow(...)) {
  await SessionCompaction.create(...)  // 不进入
}

// 3. 直接进入 normal processing,继续生成响应

总结:有 lastAssistant 但没有 lastFinished 时:

  • 不会退出循环
  • 不会触发自动压缩
  • 直接进入 normal processing,让模型继续生成/补全响应

5.2 tasks 的设计思路

tasks是part表中的数据,当part.type === "compaction" || part.type === "subtask"的时候会做特殊处理。

5.2.1 为什么只存 part,不存 message?

const task = msg.parts.filter((part) => part.type === "compaction" || part.type === "subtask")
if (task && !lastFinished) {
  tasks.push(...task)  // 存的是 part,不是 msg
}

原因:

  1. part 包含处理所需的所有信息:

    // part 结构
    {
      type: "compaction" | "subtask",
      auto: boolean,        // 是否自动触发
      messageID: string,    // 可以找到对应的 message
      sessionID: string,    // 可以找到对应的 session
      // ... 其他字段
    }
    
  2. process 函数通过其他参数获取 message:

    SessionCompaction.process({
      messages: msgs,        // ← 完整消息历史已经传入
      parentID: lastUser.id, // ← 触发压缩的用户消息 ID
      auto: task.auto,       // ← 只需要 part 中的这个字段
    })
    
  3. 避免数据冗余:msgs 数组已经包含了所有 message 数据,不需要再存储一份

5.2.2 tasks 后续如何使用?

const task = tasks.pop()  // 取最新一个待处理任务

if (task?.type === "compaction") {
  // 处理压缩任务
  await SessionCompaction.process({ auto: task.auto, ... })
}

if (task?.type === "subtask") {
  // 处理子任务
  // ...
}

设计思路:tasks 是一个待处理任务队列,按时间倒序收集,然后用 pop() 取出最新的一个进行处理。

5.2.3 没有待处理任务时的流程

如果没有 compaction 或 subtask 类型的 part:

tasks 数组为空,tasks.pop() 返回 undefined,直接进入 normal processing:

const task = tasks.pop()  // 返回 undefined

if (task?.type === "subtask") {
  // 不进入
}

if (task?.type === "compaction") {
  // 不进入
}

// context overflow - 需要 lastFinished 存在才会触发
if (lastFinished && lastFinished.summary !== true && isOverflow(...)) {
  // 如果 lastFinished 不存在,不进入
}

// normal processing - 直接进入这里
const agent = await Agent.get(lastUser.agent)
const processor = SessionProcessor.create({...})
// ... 调用 processor.process() → 大模型 API

流程图:

没有 compaction/subtask part
    ↓
task = undefined
    ↓
跳过 subtask 处理
    ↓
跳过 compaction 处理
    ↓
检查是否需要自动压缩(需要 lastFinished 存在)
    ↓
进入 normal processing
    ↓
调用 processor.process() → 大模型 API

5.2.4 未完成的 part 数据如何处理?

进入 normal processing 时,会带着未完成助手消息的 part 数据调用大模型 API,但有特殊处理:

位置:src/session/message-v2.ts:606-670

for (const part of msg.parts) {
  if (part.type === "text")
    assistantMessage.parts.push({ type: "text", text: part.text })

  if (part.type === "tool") {
    if (part.state.status === "completed") {
      // 已完成的工具调用 → 正常输出
      assistantMessage.parts.push({
        type: ("tool-" + part.tool),
        state: "output-available",
        input: part.state.input,
        output: part.state.output,
      })
    }

    // ★ 关键:未完成的工具调用 → 标记为中断
    if (part.state.status === "pending" || part.state.status === "running")
      assistantMessage.parts.push({
        type: ("tool-" + part.tool),
        state: "output-error",
        input: part.state.input,
        errorText: "[Tool execution was interrupted]",  // ← 特殊标记
      })
  }
}

处理逻辑:

part 状态 处理方式
completed 正常输出,包含 input 和 output
error 输出错误信息
pending / running 输出 "[Tool execution was interrupted]"

为什么这样处理?

// Handle pending/running tool calls to prevent dangling tool_use blocks
// Anthropic/Claude APIs require every tool_use to have a corresponding tool_result

原因:Anthropic API 要求每个 tool_use 都有对应的 tool_result。如果之前的工具调用没有完成,必须提供一个"假"的结果,否则 API 会报错。

完整流程:

有 lastAssistant 但没有 lastFinished
    ↓
msgs 包含未完成的助手消息(有 parts,无 finish)
    ↓
toModelMessages() 转换:
  - text parts → 正常包含
  - completed tools → 正常输出
  - pending/running tools → "[Tool execution was interrupted]"
    ↓
processor.process() 带着转换后的消息调用大模型 API
    ↓
模型看到之前的内容 + 中断标记,决定如何继续

6. [服务端] Compaction 上下文压缩

大模型有上下文长度限制(如 Claude 的 200K tokens),当对话历史过长时:

  1. 无法继续对话:超过模型限制会导致 API 报错
  2. 成本增加:每次请求都发送完整历史,token 消耗巨大
  3. 效率降低:模型处理大量无关历史信息

Compaction 机制通过生成对话摘要来解决这些问题,将长对话压缩为简洁的上下文摘要。

详情见OpenCode Compaction 上下文压缩 和 清理旧工具输出。

7. [服务端] 处理提示词并调用大模型

构建和使用系统提示词:

在src/session/prompt.ts的loop函数中会创建系统提示词:

const system = [...(await SystemPrompt.environment(model)), ...(await InstructionPrompt.system())]

返回的系统提示词当做processor.process函数的参数传入:

const result = await processor.process({
  ......
  system,
  ......
})

详见OpenCode 构建提示词。

调用大模型的关键组件:

组件 位置 职责
LLM.stream src/session/llm.ts 统一的大模型调用入口,整合配置、认证、工具定义
streamText Vercel AI SDK 流式调用大模型,返回统一格式的事件流
ProviderTransform src/provider/transform.ts 多平台模型适配(消息格式、缓存、推理配置等)

详情见OpenCode 调用大模型。

8. [服务端] loop循环控制逻辑

8.1 循环退出条件

位置: src/session/prompt.ts:324-331

if (
  lastAssistant?.finish &&
  !["tool-calls", "unknown"].includes(lastAssistant.finish) &&
  lastUser.id < lastAssistant.id
) {
  // 模型已完成,退出循环
  log.info("exiting loop", { sessionID })
  break
}

这段代码判断模型已经完成对用户消息的响应,不需要继续处理工具调用。三个条件必须同时满足:

条件 1:lastAssistant?.finish

最后一条助手消息有 finish 原因。finish 字段表示模型完成响应的原因,可能的值包括:

finish 值 含义
"stop" 正常结束(没有工具调用)
"tool-calls" 模型调用了工具,需要继续处理
"length" 达到最大 token 限制
"content-filter" 内容被过滤
"unknown" 未知原因

条件 2:!["tool-calls", "unknown"].includes(lastAssistant.finish)

finish 不是 "tool-calls" 或 "unknown"。

  • 排除 "tool-calls":如果模型调用了工具,循环需要继续处理工具调用结果
  • 排除 "unknown":无法确定完成原因时,不退出循环

条件 3:lastUser.id < lastAssistant.id

用户消息的 id 小于助手消息的 id。由于 id 是按时间递增生成的,这意味着助手消息是对这条用户消息的响应(而不是旧的历史记录)。

退出条件总结

当三个条件都满足时,说明:

  • 模型已完成响应(有 finish 原因)
  • 没有待处理的工具调用(finish 不是 tool-calls)
  • 这是对用户输入的完整回复(用户消息在助手消息之前)

所以可以安全退出循环。

8.2 循环继续条件

当模型返回 finish: "tool-calls" 时,循环继续,等待工具执行结果。

8.3 processor.process 返回值

  • "continue": 继续循环
  • "stop": 停止循环
  • "compact": 触发压缩后继续

8.4 最大步数限制 (maxSteps)

maxSteps 用于限制 agent 的最大执行步数,防止无限循环。

8.4.1 定义和判断

位置: src/session/prompt.ts:572-573

const maxSteps = agent.steps ?? Infinity  // 从 agent 配置读取,默认无限制
const isLastStep = step >= maxSteps       // 判断是否达到最大步数

8.4.2 达到最大步数时的处理

位置: src/session/prompt.ts:677-687

当 isLastStep = true 时,会在消息末尾注入 MAX_STEPS 提示:

messages: [
  ...MessageV2.toModelMessages(msgs, model),
  ...(isLastStep
    ? [
        {
          role: "assistant" as const,
          content: MAX_STEPS,  // ← 注入特殊提示
        },
      ]
    : []),
],

MAX_STEPS 提示内容(src/session/prompt/max-steps.txt):

CRITICAL - MAXIMUM STEPS REACHED

The maximum number of steps allowed for this task has been reached.
Tools are disabled until next user input. Respond with text only.

STRICT REQUIREMENTS:
1. Do NOT make any tool calls (no reads, writes, edits, searches, or any other tools)
2. MUST provide a text response summarizing work done so far
3. This constraint overrides ALL other instructions

Response must include:
- Statement that maximum steps for this agent have been reached
- Summary of what has been accomplished so far
- List of any remaining tasks that were not completed
- Recommendations for what should be done next

8.4.3 执行流程

┌─────────────────────────────────────────────────────────────────────────────┐
│                          Agent 执行循环                                      │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  step = 0                                                                   │
│  while (true) {                                                             │
│      step++                        // 每次循环递增                           │
│                                                                             │
│      const maxSteps = agent.steps ?? Infinity                              │
│      const isLastStep = step >= maxSteps                                   │
│                                                                             │
│      if (isLastStep) {                                                     │
│          // 在消息末尾注入 MAX_STEPS 提示                                   │
│          // 告诉模型:停止调用工具,只返回文本总结                           │
│      }                                                                      │
│                                                                             │
│      result = await processor.process(...)                                 │
│                                                                             │
│      if (result === "stop") break           // 模型主动停止                 │
│      if (finishReason !== "tool-calls") break  // 没有工具调用,停止        │
│                                                                             │
│      // 如果有工具调用,继续下一轮(step++)                                 │
│  }                                                                          │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

8.4.4 配置方式

在 agent 配置中设置 steps:

// src/agent/agent.ts:44
steps: z.number().int().positive().optional(),  // 最大步数

配置文件示例:

{
  "agent": {
    "my-agent": {
      "steps": 10,
      "description": "最多执行 10 步"
    }
  }
}

8.4.5 使用场景

场景 作用
防止无限循环 避免模型陷入重复调用工具的死循环
限制成本 控制单次任务的最大 token 消耗
强制总结 达到步数限制后,要求模型总结当前进展
安全边界 为自动化任务设置安全边界

8.4.6 示例

假设设置 steps: 5:

Step 1: 模型调用 read 工具 → 继续循环
Step 2: 模型调用 edit 工具 → 继续循环
Step 3: 模型调用 bash 工具 → 继续循环
Step 4: 模型调用 read 工具 → 继续循环
Step 5: isLastStep = true → 注入 MAX_STEPS 提示
        模型返回文本总结,不再调用工具 → 循环结束

9. [服务端 → 客户端] 服务端流式响应 和 用户确认机制

详见OpenCode 服务端流式响应 和 用户确认机制

posted @ 2026-02-19 22:01  不歪歪  阅读(272)  评论(0)    收藏  举报