摘要

本文深入分析了 OpenCode 项目中 Build 智能体的完整实现机制。Build 智能体是 OpenCode 的默认代理,承担着代码实施和执行的核心职责。与 Plan 智能体的只读特性形成鲜明对比,Build 智能体拥有完整的工具访问权限,能够执行文件编辑、命令运行、代码搜索等操作。本文从架构设计、权限系统、工作流程、工具集成和错误处理等多个维度进行了详细剖析,为理解现代 AI 编程助手的执行能力提供了参考。

关键词:OpenCode;Build 智能体;工具执行;权限管理;Effect.ts;AI 编程助手


目录

  1. 引言
  2. Build 智能体核心设计
    2.1 智能体定义与配置
    2.2 权限系统设计
    2.3 模式分类与角色
  3. 工作流程详解
    3.1 计划执行模式
    3.2 直接执行模式
    3.3 混合模式
    3.4 主动进入计划模式
  4. 工具系统集成
    4.1 文件操作工具
    4.2 命令执行工具
    4.3 代码搜索工具
    4.4 任务委托工具
  5. 权限请求与用户交互
    5.1 权限评估机制
    5.2 交互式授权
    5.3 自动接受策略
  6. 错误处理与恢复
    6.1 工具执行失败
    6.2 权限拒绝处理
    6.3 会话回滚机制
  7. 性能优化策略
    7.1 并行工具调用
    7.2 上下文管理
    7.3 缓存策略
  8. 前端集成与可视化
  9. 设计模式与最佳实践
  10. 总结与展望

1. 引言

1.1 研究背景

在 AI 编程助手系统中,规划与执行是两个核心阶段。Plan 智能体负责前期的需求分析和方案设计,而 Build 智能体则承担着将计划转化为实际代码的重任。Build 智能体需要具备以下关键能力:

  • 完整的工具访问权限:能够读取、编辑文件,执行命令
  • 灵活的执行策略:根据任务复杂度选择直接执行或重新规划
  • 健壮的错误处理:应对工具执行失败和权限拒绝
  • 高效的性能表现:通过并行调用和上下文优化提升效率

1.2 研究意义

Build 智能体的设计体现了以下软件工程理念:

  1. 最小惊讶原则:默认行为符合开发者预期
  2. 渐进式授权:根据风险等级动态请求权限
  3. 可恢复性:支持撤销和回滚操作
  4. 透明性:清晰展示执行过程和状态

1.3 研究方法

本文通过对 OpenCode 源码的深入分析,重点关注:

  • packages/opencode/src/agent/agent.ts:Build 智能体定义
  • packages/opencode/src/tool/*.ts:各种工具的实现
  • packages/opencode/src/permission/index.ts:权限评估系统
  • packages/opencode/src/session/prompt.ts:工作流程控制
  • packages/opencode/src/session/revert.ts:回滚机制

2. Build 智能体核心设计

2.1 智能体定义与配置

Build 智能体在 packages/opencode/src/agent/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,
}

关键特性

  1. 默认智能体:当用户未指定时自动使用
  2. 主智能体模式:可直接与用户交互
  3. 完整权限:继承默认权限,允许大部分操作
  4. 原生实现:系统内置,无需额外配置

2.2 权限系统设计

2.2.1 三层权限架构

Build 智能体的权限同样由三个层次组成:

第一层:默认权限(defaults)

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 文件需要询问
    "*.env.*": "ask",
    "*.env.example": "allow",      // 示例文件允许
  },
})

第二层:Build 特定权限

Permission.fromConfig({
  question: "allow",     // 允许向用户提问
  plan_enter: "allow",   // 允许进入计划模式
})

第三层:用户配置权限

const user = Permission.fromConfig(cfg.permission ?? {})

用户可以全局覆盖某些权限,例如:

{
  "permission": {
    "bash": "ask",       // 所有命令执行都需要确认
    "edit": "allow"      // 允许所有编辑操作
  }
}
2.2.2 权限合并算法
permission: Permission.merge(defaults, buildSpecific, user)

Permission.merge 的实现逻辑:

// packages/opencode/src/permission/index.ts
export function merge(...rulesets: Ruleset[]): Ruleset {
  const merged: Rule[] = []
  
  for (const ruleset of rulesets) {
    for (const rule of ruleset) {
      // 检查是否已存在相同权限和模式的规则
      const existingIndex = merged.findIndex(
        r => r.permission === rule.permission && r.pattern === rule.pattern
      )
      
      if (existingIndex !== -1) {
        // 后定义的规则覆盖先前的规则
        merged[existingIndex] = rule
      } else {
        merged.push(rule)
      }
    }
  }
  
  return merged
}

优先级策略

  1. 最后定义优先:后续层的规则会覆盖前面的规则
  2. 明确性优先:具体路径匹配优先于通配符
  3. 安全性保障:用户可以通过配置收紧权限,但不能突破系统限制
2.2.3 权限评估流程
// packages/opencode/src/permission/evaluate.ts
export function evaluate(
  permission: string,
  pattern: string,
  ...rulesets: Ruleset[]
): Rule {
  const allRules = rulesets.flat()
  
  // 收集所有匹配的规则
  const matchingRules = allRules.filter(rule => 
    Wildcard.match(permission, rule.permission) &&
    Wildcard.match(pattern, rule.pattern)
  )
  
  if (matchingRules.length === 0) {
    // 没有匹配规则,返回默认允许
    return { permission, pattern, action: "allow" }
  }
  
  // 按特异性排序(具体路径优先)
  matchingRules.sort((a, b) => {
    const aSpecificity = specificity(a.pattern)
    const bSpecificity = specificity(b.pattern)
    return bSpecificity - aSpecificity
  })
  
  // 返回最具体的规则
  return matchingRules[0]
}

function specificity(pattern: string): number {
  // 计算模式的具体性得分
  // 通配符越少,得分越高
  return pattern.split("*").length - 1
}

评估示例

// 场景:Build agent 尝试编辑 src/component.tsx
evaluate("edit", "src/component.tsx", defaults, buildSpecific, user)

// 匹配的规则:
// 1. { permission: "*", pattern: "*", action: "allow" }  (来自 defaults)
// 2. { permission: "edit", pattern: "*", action: "allow" }  (隐含)

// 结果:allow

2.3 模式分类与角色

2.3.1 Primary Agent 特性

Build 智能体作为 primary agent,具有以下特性:

  1. 直接用户交互:可以接收用户输入并直接响应
  2. 工具调用权限:可以使用所有注册的工具
  3. 会话管理:可以创建和管理会话消息
  4. 子代理调度:可以调用 subagents 完成子任务
2.3.2 与其他智能体的关系
┌─────────────────────────────────────┐
│         User Input                  │
└──────────┬──────────────────────────┘
           │
    ┌──────▼───────┐
    │ Build Agent  │ ◄── Primary Agent
    └──────┬───────┘
           │
     ┌─────┴─────┐
     │           │
┌────▼────┐ ┌───▼────────┐
│ Explore │ │  General    │ ◄── Subagents
│ Agent   │ │  Agent      │
└─────────┘ └────────────┘
     │           │
     └─────┬─────┘
           │
    ┌──────▼───────┐
    │ Plan Agent   │ ◄── Primary Agent (optional)
    └──────────────┘

协作模式

  • Build → Explore:快速探索代码库
  • Build → General:复杂研究和多步任务
  • Build → Plan:发现任务复杂时切换到规划模式

3. 工作流程详解

Build 智能体的工作流程根据任务类型和用户意图动态调整,主要分为三种模式:计划执行模式、直接执行模式和混合模式。

3.1 计划执行模式

当 Build 智能体检测到存在计划文件时,会自动进入计划执行模式。

3.1.1 模式切换检测
// packages/opencode/src/session/prompt.ts
if (input.agent.name !== "plan" && assistantMessage?.info.agent === "plan") {
  const plan = Session.plan(input.session)
  if (!(yield* fsys.existsSafe(plan))) return input.messages
  
  const part = yield* sessions.updatePart({
    id: PartID.ascending(),
    messageID: userMessage.info.id,
    sessionID: userMessage.info.sessionID,
    type: "text",
    text: BUILD_SWITCH + "\n\n" + `A plan file exists at ${plan}. You should execute on the plan defined within it`,
    synthetic: true,
  })
  userMessage.parts.push(part)
  return input.messages
}

触发条件

  1. 当前智能体不是 plan(即 build 或其他)
  2. 上一条助手消息来自 plan 智能体
  3. 计划文件存在

BUILD_SWITCH 提示词

<system-reminder>
Your operational mode has changed from plan to build.
You are no longer in read-only mode.
You are permitted to make file changes, run shell commands, and utilize your arsenal of tools as needed.
</system-reminder>

系统行为

  • 自动注入合成文本部分(synthetic: true)
  • 通知 Build 智能体权限已变更
  • 指示智能体按照计划文件执行
3.1.2 计划执行流程
用户批准计划
    ↓
plan_exit 工具执行
    ↓
创建新的 User 消息(agent: "build")
    ↓
注入 BUILD_SWITCH 提示词
    ↓
Build 智能体接收上下文
    ↓
读取计划文件
    ↓
按步骤执行实施
    ↓
验证完成

典型对话流

Plan Agent: [完成计划撰写,调用 plan_exit]
System: [显示确认对话框]
User: Yes, switch to build agent
System: [创建新消息,agent="build", 注入提示词]
Build Agent: [读取计划文件,开始执行]
  ├─ Read: .opencode/plans/xxx.md
  ├─ Edit: src/component.tsx
  ├─ Bash: npm test
  └─ Write: src/utils.ts

3.2 直接执行模式

对于简单任务,Build 智能体直接进入执行模式,无需经过规划阶段。

3.2.1 任务复杂度判断

Build 智能体根据以下因素判断是否需要规划:

适合直接执行的任务

  • 单文件修改(typo 修复、变量重命名)
  • 简单的配置更改
  • 明确的命令执行
  • 小范围的代码重构

需要规划的任务

  • 涉及多个文件的架构变更
  • 新功能开发
  • 复杂的 bug 修复
  • 需要调研的技术选型
3.2.2 执行策略
// 伪代码示例
async function executeTask(task: Task) {
  if (isSimple(task)) {
    // 直接执行
    await directExecute(task)
  } else {
    // 建议用户切换到 plan 模式
    await suggestPlanning(task)
  }
}

function isSimple(task: Task): boolean {
  return (
    task.files.length <= 1 &&
    task.complexity < THRESHOLD &&
    !task.requiresResearch
  )
}

3.3 混合模式

在实际使用中,Build 智能体经常在两种模式之间切换。

3.3.1 迭代优化流程
Build 执行 Step 1
    ↓
发现问题需要重新设计
    ↓
调用 plan_enter
    ↓
Plan 重新规划
    ↓
plan_exit 返回 Build
    ↓
继续执行 Step 2
3.3.2 增量实施
读取计划文件
    ↓
执行 Step 1
    ↓
提交更改
    ↓
验证 Step 1
    ↓
执行 Step 2
    ↓
...

3.4 主动进入计划模式

Build 智能体可以在执行过程中发现任务复杂时主动切换到 Plan 模式。

3.4.1 plan_enter 工具(概念性)

虽然当前代码中 plan_enter 工具被注释掉,但其设计理念值得关注:

/*
export const PlanEnterTool = Tool.define("plan_enter", {
  description: ENTER_DESCRIPTION,
  parameters: z.object({}),
  async execute(_params, ctx) {
    const session = await Session.get(ctx.sessionID)
    const plan = path.relative(Instance.worktree, Session.plan(session))

    const answers = await Question.ask({
      sessionID: ctx.sessionID,
      questions: [
        {
          question: `Would you like to switch to the plan agent and create a plan saved to ${plan}?`,
          header: "Plan Mode",
          custom: false,
          options: [
            { label: "Yes", description: "Switch to plan agent for research and planning" },
            { label: "No", description: "Stay with build agent to continue making changes" },
          ],
        },
      ],
      tool: ctx.callID ? { messageID: ctx.messageID, callID: ctx.callID } : undefined,
    })

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

    const model = await getLastModel(ctx.sessionID)

    const userMsg: MessageV2.User = {
      id: MessageID.ascending(),
      sessionID: ctx.sessionID,
      role: "user",
      time: { created: Date.now() },
      agent: "plan",
      model,
    }
    await Session.updateMessage(userMsg)
    await Session.updatePart({
      id: PartID.ascending(),
      messageID: userMsg.id,
      sessionID: ctx.sessionID,
      type: "text",
      text: "User has requested to enter plan mode. Switch to plan mode and begin planning.",
      synthetic: true,
    })

    return {
      title: "Switching to plan agent",
      output: `User confirmed to switch to plan mode. A new message has been created to switch you to plan mode. The plan file will be at ${plan}. Begin planning.`,
      metadata: {},
    }
  },
})
*/

使用场景

User: 帮我添加用户认证功能

Build Agent: [评估任务复杂度]
  ├─ 需要设计认证方案
  ├─ 需要选择认证提供商
  ├─ 需要修改多个文件
  └─ 需要数据库 schema 变更
  
Build Agent: [调用 plan_enter 工具]
  → 切换到 Plan 模式进行详细规划

4. 工具系统集成

Build 智能体可以访问多种工具来完成代码实施任务。

4.1 文件操作工具

4.1.1 Read 工具
// packages/opencode/src/tool/read.ts
export const ReadTool = Tool.define("read", {
  description: "Read the contents of a file",
  parameters: z.object({
    filePath: z.string(),
    offset: z.number().optional(),
    limit: z.number().optional(),
  }),
  async execute(params, ctx) {
    const content = await fs.readFile(params.filePath, "utf-8")
    const lines = content.split("\n")
    
    const start = params.offset ?? 0
    const end = params.limit ? start + params.limit : lines.length
    
    return {
      title: `Read ${params.filePath}`,
      output: lines.slice(start, end).join("\n"),
      metadata: {
        totalLines: lines.length,
        shownLines: end - start,
      },
    }
  },
})

使用场景

  • 理解现有代码结构
  • 查看配置文件
  • 分析依赖关系
4.1.2 Edit 工具
// packages/opencode/src/tool/edit.ts
export const EditTool = Tool.define("edit", {
  description: "Edit specific sections of a file",
  parameters: z.object({
    filePath: z.string(),
    edits: z.array(z.object({
      oldText: z.string(),
      newText: z.string(),
    })),
  }),
  async execute(params, ctx) {
    let content = await fs.readFile(params.filePath, "utf-8")
    
    for (const edit of params.edits) {
      if (!content.includes(edit.oldText)) {
        throw new Error(`Could not find text to replace in ${params.filePath}`)
      }
      content = content.replace(edit.oldText, edit.newText)
    }
    
    await fs.writeFile(params.filePath, content)
    
    return {
      title: `Edited ${params.filePath}`,
      output: `Applied ${params.edits.length} edit(s)`,
      metadata: {
        filePath: params.filePath,
        editsCount: params.edits.length,
      },
    }
  },
})

安全机制

  1. 精确匹配:必须完全匹配旧文本
  2. 原子操作:所有编辑成功后才写入文件
  3. 权限检查:在编辑前验证权限
4.1.3 Write 工具
// packages/opencode/src/tool/write.ts
export const WriteTool = Tool.define("write", {
  description: "Write content to a new file or overwrite an existing file",
  parameters: z.object({
    filePath: z.string(),
    content: z.string(),
  }),
  async execute(params, ctx) {
    await fs.ensureDir(path.dirname(params.filePath))
    await fs.writeFile(params.filePath, params.content)
    
    return {
      title: `Wrote ${params.filePath}`,
      output: `Created/overwrote file with ${params.content.length} characters`,
      metadata: {
        filePath: params.filePath,
        size: params.content.length,
      },
    }
  },
})

4.2 命令执行工具

4.2.1 Bash 工具
// packages/opencode/src/tool/bash.ts
export const BashTool = Tool.define("bash", {
  description: "Execute a bash command",
  parameters: z.object({
    command: z.string(),
    description: z.string().optional(),
    workdir: z.string().optional(),
  }),
  async execute(params, ctx) {
    const { stdout, stderr, exitCode } = await exec(params.command, {
      cwd: params.workdir ?? Instance.worktree,
      timeout: 30000,
    })
    
    return {
      title: params.description ?? `Executed: ${params.command}`,
      output: stdout || stderr,
      metadata: {
        exitCode,
        stdout,
        stderr,
      },
    }
  },
})

安全措施

  1. 超时限制:默认 30 秒超时
  2. 工作目录限制:限制在项目目录内执行
  3. 权限询问:高风险命令需要用户确认

典型用法

// 安装依赖
{
  "tool": "bash",
  "input": {
    "command": "npm install axios",
    "description": "Install axios package"
  }
}

// 运行测试
{
  "tool": "bash",
  "input": {
    "command": "npm test",
    "description": "Run test suite"
  }
}

// Git 操作
{
  "tool": "bash",
  "input": {
    "command": "git status",
    "description": "Check git status"
  }
}

4.3 代码搜索工具

4.3.1 Grep 工具
// packages/opencode/src/tool/grep.ts
export const GrepTool = Tool.define("grep", {
  description: "Search for patterns in files",
  parameters: z.object({
    pattern: z.string(),
    path: z.string().optional(),
    caseSensitive: z.boolean().optional(),
  }),
  async execute(params, ctx) {
    const results = await grep(params.pattern, {
      path: params.path ?? Instance.worktree,
      caseSensitive: params.caseSensitive ?? false,
    })
    
    return {
      title: `Searched for "${params.pattern}"`,
      output: results.map(r => `${r.file}:${r.line}: ${r.match}`).join("\n"),
      metadata: {
        matchCount: results.length,
        files: [...new Set(results.map(r => r.file))],
      },
    }
  },
})
4.3.2 Glob 工具
// packages/opencode/src/tool/glob.ts
export const GlobTool = Tool.define("glob", {
  description: "Find files matching a glob pattern",
  parameters: z.object({
    pattern: z.string(),
    path: z.string().optional(),
  }),
  async execute(params, ctx) {
    const files = await glob(params.pattern, {
      cwd: params.path ?? Instance.worktree,
    })
    
    return {
      title: `Found files matching "${params.pattern}"`,
      output: files.join("\n"),
      metadata: {
        fileCount: files.length,
      },
    }
  },
})

4.4 任务委托工具

4.4.1 Task 工具
// packages/opencode/src/tool/task.ts
export const TaskTool = Tool.define("task", {
  description: "Delegate a subtask to another agent",
  parameters: z.object({
    description: z.string(),
    agent: z.string(),
    prompt: z.string(),
  }),
  async execute(params, ctx) {
    const result = await Agent.generate({
      agent: params.agent,
      prompt: params.prompt,
      context: ctx,
    })
    
    return {
      title: `Delegated to ${params.agent}: ${params.description}`,
      output: result.output,
      metadata: {
        agent: params.agent,
        tokensUsed: result.tokens,
      },
    }
  },
})

使用场景

// 委托给 explore agent 进行代码探索
{
  "tool": "task",
  "input": {
    "description": "Find authentication implementations",
    "agent": "explore",
    "prompt": "Search for all authentication-related code in the project"
  }
}

// 委托给 general agent 进行复杂分析
{
  "tool": "task",
  "input": {
    "description": "Analyze database schema",
    "agent": "general",
    "prompt": "Review the database models and suggest improvements"
  }
}

5. 权限请求与用户交互

5.1 权限评估机制

5.1.1 权限检查流程
// packages/opencode/src/permission/index.ts
const ask = Effect.fn("Permission.ask")(function* (input: z.infer<typeof AskInput>) {
  const { approved, pending } = yield* InstanceState.get(state)
  const { ruleset, ...request } = input
  let needsAsk = false

  for (const pattern of request.patterns) {
    const rule = evaluate(request.permission, pattern, ruleset, approved)
    log.info("evaluated", { permission: request.permission, pattern, action: rule })
    
    if (rule.action === "deny") {
      return yield* new DeniedError({
        ruleset: ruleset.filter((rule) => Wildcard.match(request.permission, rule.permission)),
      })
    }
    if (rule.action === "allow") continue
    needsAsk = true
  }

  if (!needsAsk) return

  // 需要用户确认
  const id = request.id ?? PermissionID.ascending()
  const info: Request = { id, ...request }
  
  const deferred = yield* Deferred.make<void, RejectedError | CorrectedError>()
  pending.set(id, { info, deferred })
  yield* bus.publish(Event.Asked, info)
  
  return yield* Effect.ensuring(
    Deferred.await(deferred),
    Effect.sync(() => {
      pending.delete(id)
    }),
  )
})

评估逻辑

  1. 遍历所有模式:检查每个文件路径的权限
  2. 立即拒绝:如果任何模式被拒绝,直接抛出错误
  3. 跳过允许:如果所有模式都允许,无需询问
  4. 需要询问:如果有模式需要询问,暂停执行等待用户响应
5.1.2 权限规则匹配
// 示例:评估对 src/config.ts 的编辑权限
evaluate("edit", "src/config.ts", [
  { permission: "*", pattern: "*", action: "allow" },
  { permission: "edit", pattern: "*.env", action: "ask" },
])

// 匹配过程:
// 1. "*" matches "edit" AND "*" matches "src/config.ts" → allow
// 2. "edit" matches "edit" BUT "*.env" does NOT match "src/config.ts" → skip

// 结果:allow

5.2 交互式授权

5.2.1 权限请求事件
// packages/opencode/src/permission/index.ts
export const Event = {
  Asked: BusEvent.define("permission.asked", Request),
  Replied: BusEvent.define("permission.replied", z.object({
    sessionID: SessionID.zod,
    requestID: PermissionID.zod,
    reply: Reply,
  })),
}

前端监听

// packages/app/src/context/permission.tsx
function PermissionProvider({ children }) {
  const [pendingRequests, setPendingRequests] = useState([])
  
  useEffect(() => {
    const unsubscribe = bus.subscribe("permission.asked", (request) => {
      setPendingRequests(prev => [...prev, request])
    })
    return unsubscribe
  }, [])
  
  return (
    <PermissionContext.Provider value={{ pendingRequests, reply }}>
      {children}
      {pendingRequests.map(request => (
        <PermissionDialog key={request.id} request={request} />
      ))}
    </PermissionContext.Provider>
  )
}
5.2.2 用户响应选项
export const Reply = z.enum(["once", "always", "reject"])

响应含义

  • once:仅此次允许
  • always:总是允许(添加到 approved 列表)
  • reject:拒绝(并取消同一会话中的其他待处理请求)

5.3 自动接受策略

5.3.1 信任机制

用户可以配置自动接受某些权限:

{
  "permission": {
    "edit": "allow",
    "bash": {
      "npm test": "allow",
      "git status": "allow",
      "*": "ask"
    }
  }
}
5.3.2 会话级记忆
// 用户选择 "always" 后
if (input.reply === "always") {
  for (const pattern of existing.info.always) {
    approved.push({
      permission: existing.info.permission,
      pattern,
      action: "allow",
    })
  }
}

持久化

// 会话结束时保存
yield* Database.use((db) =>
  db.insert(PermissionTable)
    .values({
      project_id: ctx.project.id,
      data: state.approved,
    })
    .onConflictDoUpdate({
      target: PermissionTable.project_id,
      set: { data: state.approved },
    })
)

6. 错误处理与恢复

6.1 工具执行失败

6.1.1 错误类型
// 常见错误类型
class ToolExecutionError extends Error {
  constructor(
    public tool: string,
    public params: any,
    public cause: Error
  ) {
    super(`Tool ${tool} failed: ${cause.message}`)
  }
}

class FileNotFound
Logo

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

更多推荐