摘要

OpenCode是一个基于大语言模型的AI编程助手框架,其核心创新在于多智能体协作系统的设计。本文深入分析了OpenCode中Plan智能体和Build智能体的实现机制,揭示了其在任务规划与执行解耦、权限控制、工具调用管理等方面的架构设计。

关键词:智能体系统、任务规划、大语言模型、权限控制、工具调用


1. 系统架构概述

OpenCode采用Effect函数式编程框架构建整个系统,核心模块包括:

模块 路径 功能
Agent packages/opencode/src/agent/agent.ts 智能体定义与管理
Session packages/opencode/src/session/ 会话状态管理
LLM packages/opencode/src/session/llm.ts 大语言模型调用封装
Tool packages/opencode/src/tool/ 工具注册与执行
Permission packages/opencode/src/permission/ 权限控制

1.1 架构层次图

Agent Service

build

Permission Ruleset

plan

general

explore

Tool Registry
bash | read | edit | write | grep | glob | task | ...

LLM Service
(streamText, tool execution)


2. Build智能体实现机制

2.1 智能体定义

Build智能体在agent.ts定义为:

build: {
  name: "build",
  description: "The default agent. Executes tools based on configured permissions.",
  options: {},
  permission: Permission.merge(
    defaults,
    Permission.fromConfig({
      question: "allow",
      plan_enter: "allow",
    }),
    user,
  ),
  mode: "primary",
  native: true,
}

关键特性

  • mode: "primary" - 作为主要执行智能体
  • native: true - 内置原生智能体
  • 权限配置:允许使用question和plan_enter工具

2.2 权限系统

Build智能体的权限规则通过Permission.merge组合三层配置:

2.2.1 默认规则 (agent.ts)
const defaults = Permission.fromConfig({
  "*": "allow",
  doom_loop: "ask",
  external_directory: {
    "*": "ask",
    ...Object.fromEntries(whitelistedDirs.map((dir) => [dir, "allow"])),
  },
  question: "deny",
  plan_enter: "deny",
  plan_exit: "deny",
  read: {
    "*": "allow",
    "*.env": "ask",
    "*.env.*": "ask",
    "*.env.example": "allow",
  },
})

权限评估机制在permission/index.ts实现:

export function evaluate(permission: string, pattern: string, ...rulesets: Ruleset[]): Rule {
  return evalRule(permission, pattern, ...rulesets)
}
2.2.2 权限动作类型
动作 含义
allow 允许执行,无需确认
deny 拒绝执行
ask 询问用户确认

2.3 工具调用流程

Build智能体的工具执行通过SessionPrompt.resolveTools管理:

  1. ToolRegistry获取可用工具列表
  2. 根据权限过滤可用工具
  3. 包装为AI SDK格式的工具定义
  4. 在执行上下文中注入权限检查
// session/prompt.ts
const resolveTools = Effect.fn("SessionPrompt.resolveTools")(function* (input) {
  const tools: Record<string, AITool> = {}

  for (const item of yield* registry.tools({...})) {
    tools[item.id] = tool({
      id: item.id,
      description: item.description,
      inputSchema: jsonSchema(schema),
      execute(args, options) {
        // 权限检查
        yield* permission.ask({...})
        // 执行工具
        const result = yield* Effect.promise(() => item.execute(args, ctx))
        return result
      },
    })
  }
  return tools
})

3. Plan智能体实现机制

3.1 智能体定义

Plan智能体在agent.ts定义:

plan: {
  name: "plan",
  description: "Plan mode. Disallows all edit tools.",
  permission: Permission.merge(
    defaults,
    Permission.fromConfig({
      question: "allow",
      plan_exit: "allow",
      external_directory: {
        [path.join(Global.Path.data, "plans", "*")]: "allow",
      },
      edit: {
        "*": "deny",
        [path.join(".opencode", "plans", "*.md")]: "allow",
      },
    }),
    user,
  ),
  mode: "primary",
  native: true,
}

关键限制

  • 禁止所有编辑工具:通过edit: { "*": "deny" }实现
  • 仅允许编辑计划文件.opencode/plans/*.md
  • 允许使用question工具:用于向用户提问

3.2 Plan模式工作流程

Plan模式的工作流程在session/prompt.tsinsertReminders函数中定义,包含五个阶段:

Phase 1: 初始理解

1. 使用explore子智能体并行探索代码库(最多3个)
2. 每个explore智能体有特定搜索焦点
3. 使用question工具澄清需求

Phase 2: 设计

1. 启动general智能体设计实现方案
2. 最多1个并行智能体
3. 提供Phase 1探索结果的上下文

Phase 3: 评审

1. 读取关键文件验证方案
2. 确保与用户意图一致
3. 使用question工具澄清问题

Phase 4: 最终计划

1. 将计划写入计划文件
2. 仅包含推荐方案,不包含替代方案
3. 包含关键文件路径和验证方法

Phase 5: 调用plan_exit

1. 标记计划完成
2. 请求用户审批
3. 用户批准后切换到Build智能体

3.3 Plan Exit工具实现

tool/plan.ts定义了PlanExitTool

// src/tool/plan.ts
export const PlanExitTool = Tool.define("plan_exit", {
  description: EXIT_DESCRIPTION,
  parameters: z.object({}),
  async execute(_params, ctx) {
    const answers = await Question.ask({
      sessionID: ctx.sessionID,
      questions: [
        {
          question: `Plan at ${plan} is complete. Would you like to switch to the build agent?`,
          header: "Build Agent",
          options: [
            { label: "Yes", description: "Switch to build agent and start implementing" },
            { label: "No", description: "Stay with plan agent to continue refining" },
          ],
        },
      ],
    })

    const answer = answers[0]?.[0]
    if (answer === "No") throw new Question.RejectedError()

    // 用户选择Yes,创建build类型的消息切换智能体
    const userMsg: MessageV2.User = {
      id: MessageID.ascending(),
      sessionID: ctx.sessionID,
      role: "user",
      agent: "build", // 关键:切换到build智能体
      model,
    }
    await Session.updateMessage(userMsg)

    return {
      title: "Switching to build agent",
      output: "User approved switching to build agent.",
    }
  },
})

3.4 计划文件管理

计划文件路径由Session.plan()函数生成:

// session/index.ts
export function plan(input: { slug: string; time: { created: number } }) {
  const base = Instance.project.vcs
    ? path.join(Instance.worktree, ".opencode", "plans")
    : path.join(Global.Path.data, "plans")
  return path.join(base, [input.time.created, input.slug].join("-") + ".md")
}

4. 子智能体系统

4.1 Subagent架构

OpenCode通过task工具支持子智能体调用:

// tool/task.ts
export const TaskTool = Tool.defineEffect(id, Effect.gen(function* () {
  const run = Effect.fn("TaskTool.execute")(function* (params, ctx) {
    // 获取子智能体配置
    const next = yield* agent.get(params.subagent_type)

    // 创建子会话
    const nextSession = yield* Session.create({
      parentID: ctx.sessionID,
      title: params.description + ` (@${next.name} subagent)`,
      permission: [...],
    })

    // 执行子任务
    const result = yield* SessionPrompt.prompt({
      sessionID: nextSession.id,
      agent: next.name,
      ...
    })

    return { output: result.parts[...].text, ... }
  })
}))

4.2 预定义子智能体

智能体名称 用途 权限范围 模式
general 通用研究和多步骤任务执行 大部分工具 subagent
explore 代码库快速探索 仅读取工具 subagent
compaction 会话压缩生成摘要 无工具 primary
title 生成会话标题 无工具 primary
summary 生成会话摘要 无工具 primary

4.3 Explore智能体提示词

// src/agent/prompt/explore.txt
You are a file search specialist. You excel at thoroughly navigating
and exploring codebases.

Your strengths:
- Rapidly finding files using glob patterns
- Searching code and text with powerful regex patterns
- Reading and analyzing file contents

Guidelines:
- Use Glob for broad file pattern matching
- Use Grep for searching file contents
- Do not create any files or modify system state

4.4 智能体模式对比

模式 描述 使用场景
primary 主智能体,直接响应用户 build、plan
subagent 子智能体,通过task工具调用 explore、general
all 灵活模式 自定义智能体

5. 关键实现机制

5.1 会话处理流程

SessionProcessorsession/processor.ts)处理LLM流式响应:

// session/processor.ts
const handleEvent = Effect.fn("SessionProcessor.handleEvent")(function* (value) {
  switch (value.type) {
    case "start":
      yield* status.set(ctx.sessionID, { type: "busy" })
      return

    case "tool-call":
      // 检测doom loop
      if (recentParts.length === DOOM_LOOP_THRESHOLD) {
        yield* permission.ask({ permission: "doom_loop", ... })
      }
      return

    case "tool-result":
      yield* completeToolCall(value.toolCallId, value.output)
      return

    case "finish-step":
      // 更新token计数,检查溢出
      if (isOverflow({ tokens: usage.tokens, model: ctx.model })) {
        ctx.needsCompaction = true
      }
      return
  }
})

5.2 上下文溢出处理

当token数量超过阈值时触发压缩:

// session/compaction.ts
export const PRUNE_MINIMUM = 20_000
export const PRUNE_PROTECT = 40_000
const PRUNE_PROTECTED_TOOLS = ["skill"]

const prune = Effect.fn("SessionCompaction.prune")(function* (input) {
  let total = 0
  const toPrune: MessageV2.ToolPart[] = []

  // 遍历消息,收集需要清理的旧工具输出
  loop: for (let msgIndex = msgs.length - 1; msgIndex >= 0; msgIndex--) {
    for (let partIndex = msg.parts.length - 1; partIndex >= 0; partIndex--) {
      const part = msg.parts[partIndex]
      if (part.type === "tool" && part.state.status === "completed") {
        const estimate = Token.estimate(part.state.output)
        total += estimate
        if (total > PRUNE_PROTECT) {
          toPrune.push(part)
        }
      }
    }
  }

  // 清理选定的工具输出
  if (pruned > PRUNE_MINIMUM) {
    for (const part of toPrune) {
      part.state.time.compacted = Date.now()
    }
  }
})

5.3 消息转换

MessageV2.toModelMessagesEffect将内部消息格式转换为AI SDK格式:

// session/message-v2.ts
export const toModelMessagesEffect = Effect.fnUntraced(function* (input: WithParts[], model: Provider.Model) {
  const result: UIMessage[] = []

  for (const msg of input) {
    if (msg.info.role === "user") {
      // 转换用户消息
      for (const part of msg.parts) {
        if (part.type === "text") {
          userMessage.parts.push({ type: "text", text: part.text })
        }
        if (part.type === "file" && !isMedia(part.mime)) {
          userMessage.parts.push({ type: "file", url: part.url, mediaType: part.mime })
        }
      }
    }

    if (msg.info.role === "assistant") {
      // 转换助手消息
      for (const part of msg.parts) {
        if (part.type === "tool" && 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: part.state.output,
          })
        }
      }
    }
  }

  return yield* Effect.promise(() => convertToModelMessages(result, { tools }))
})

5.4 工具执行上下文

工具执行上下文定义在tool/tool.ts

// src/tool/tool.ts
export type Context<M extends Metadata = Metadata> = {
  sessionID: SessionID
  messageID: MessageID
  agent: string
  abort: AbortSignal
  callID?: string
  extra?: { [key: string]: any }
  messages: MessageV2.WithParts[]
  metadata(input: { title?: string; metadata?: M }): void
  ask(input: Omit<Permission.Request, "id" | "sessionID" | "tool">): Promise<void>
}

6. 架构优势分析

6.1 Plan-Build解耦设计

Build Agent

读取计划文件
执行编辑操作
使用全部工具

Plan Agent

Phase 1: 探索代码库(explore子智能体并行)
Phase 2: 设计方案(general子智能体)
Phase 3: 评审验证
Phase 4: 写入计划文件
Phase 5: 调用plan_exit

User Request

用户审批

6.2 权限粒度控制

通过规则集实现细粒度权限控制:

// 权限规则示例
const ruleset = [
  { permission: "*", pattern: "*", action: "allow" }, // 允许所有
  { permission: "edit", pattern: "*", action: "deny" }, // 禁止编辑
  { permission: "edit", pattern: ".opencode/plans/*.md", action: "allow" }, // 例外
  { permission: "*.env", pattern: "*", action: "ask" }, // 环境文件询问
]

6.3 工具截断机制

Truncate.output防止工具输出过长:

// tool/tool.ts
toolInfo.execute = async (args, ctx) => {
  const result = await execute(args, ctx)
  const truncated = await Truncate.output(result.output, {}, agent)
  return {
    ...result,
    output: truncated.content,
    metadata: {
      ...result.metadata,
      truncated: truncated.truncated,
      ...(truncated.truncated && { outputPath: truncated.outputPath }),
    },
  }
}

6.4 Doom Loop检测

防止智能体在相同工具调用上无限循环:

// session/processor.ts
case "tool-call": {
  const recentParts = parts.slice(-DOOM_LOOP_THRESHOLD)

  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",
      patterns: [value.toolName],
      sessionID: ctx.sessionID,
      ...
    })
  }
  return
}

7. 数据模型

7.1 会话数据库结构

-- session表
CREATE TABLE session (
  id TEXT PRIMARY KEY,
  project_id TEXT NOT NULL REFERENCES project(id),
  parent_id TEXT,
  title TEXT NOT NULL,
  permission TEXT,  -- JSON Ruleset
  time_created INTEGER,
  time_updated INTEGER,
  time_compacting INTEGER,
  time_archived INTEGER
);

-- message表
CREATE TABLE message (
  id TEXT PRIMARY KEY,
  session_id TEXT NOT NULL REFERENCES session(id),
  time_created INTEGER,
  data TEXT NOT NULL  -- JSON MessageInfo
);

-- part表
CREATE TABLE part (
  id TEXT PRIMARY KEY,
  message_id TEXT NOT NULL REFERENCES message(id),
  session_id TEXT NOT NULL,
  time_created INTEGER,
  data TEXT NOT NULL  -- JSON Part
);

7.2 消息类型层次

MessageV2.Part
├── TextPart          -- 文本内容
├── ToolPart         -- 工具调用
│   ├── pending
│   ├── running
│   ├── completed
│   └── error
├── ReasoningPart    -- 推理过程
├── FilePart         -- 文件附件
├── StepStartPart    -- 步骤开始
├── StepFinishPart   -- 步骤完成
├── SnapshotPart     -- 快照
├── PatchPart        -- 补丁
├── AgentPart        -- 智能体切换
├── SubtaskPart      -- 子任务
├── CompactionPart   -- 压缩标记
└── RetryPart        -- 重试标记

8. 结论

OpenCode的Plan和Build智能体系统展示了一个精心设计的AI编程助手架构:

8.1 核心创新

  1. 职责分离:Plan智能体专注规划,Build智能体专注执行
  2. 权限隔离:通过规则集实现工具级别的访问控制
  3. 子智能体协作:支持专业化子智能体处理特定任务
  4. 上下文管理:自动压缩和清理机制保证长会话可用性

8.2 技术亮点

特性 实现位置 说明
Effect框架 全局 函数式编程,依赖注入
权限系统 permission/ 通配符模式,规则集
工具注册 tool/registry.ts 动态加载,插件支持
会话压缩 session/compaction.ts 自动上下文管理
Doom Loop检测 session/processor.ts 防止无限循环

8.3 设计启示

  1. 分层解耦:智能体、权限、工具分离,便于扩展
  2. 规则驱动:权限和配置通过规则描述,而非硬编码
  3. 事件流处理:使用流式处理管理LLM响应
  4. 可组合性:通过Effect的Layer实现模块组合

这种架构为构建复杂的多智能体AI系统提供了优秀的参考实现。


参考文献

文件 路径 说明
智能体定义 packages/opencode/src/agent/agent.ts Agent服务核心实现
工具注册 packages/opencode/src/tool/registry.ts 工具发现和初始化
会话处理 packages/opencode/src/session/processor.ts LLM流式响应处理
权限系统 packages/opencode/src/permission/index.ts 权限评估和请求
Plan工具 packages/opencode/src/tool/plan.ts 计划退出工具
会话压缩 packages/opencode/src/session/compaction.ts 上下文溢出处理
消息管理 packages/opencode/src/session/message-v2.ts 消息格式转换

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐