跳到主要内容

第 12 章 · 扩展开发:定制你的 Agent

理解了架构,扩展就是水到渠成的事。本章三个实战覆盖最常见的扩展需求。

12.1 实战 1:新增自定义工具(完整清单)​

以「目录树」工具 list_dir_tree 为例(比 shell ls 更结构化):

① 新建 agent-core/tools/list-dir-tree.ts:

import { promises as fs } from 'fs'
import path from 'path'
import type { ToolDefinition, ToolHandler } from '../../src/types'
import { validatePath } from '../sandbox'

export const listDirTreeDefinition: ToolDefinition = {
type: 'function',
function: {
name: 'list_dir_tree',
description:
'以树状结构列出工作区内某目录的所有文件与子目录(自动忽略 node_modules/.git)。' +
'需要了解项目结构时调用,比逐个 file_read 更高效。',
parameters: {
type: 'object',
properties: {
dir: {
type: 'string',
description: '相对于工作区的目录路径,默认 "."',
},
maxDepth: {
type: 'integer',
description: '最大深度,默认 3',
},
},
required: [],
},
},
}

export const listDirTreeHandler: ToolHandler = async (args, context) => {
const dir = String(args.dir ?? '.')
const maxDepth = Math.min(Number(args.maxDepth ?? 3) || 3, 5)

const root = path.resolve(context.workspacePath, dir)
if (!validatePath(context.workspacePath, root)) {
return '错误:路径越界' // 安全:复用沙箱
}

const IGNORE = new Set(['node_modules', '.git', 'dist'])
const lines: string[] = [dir]

async function walk(current: string, depth: number, prefix: string) {
if (depth > maxDepth) return
const entries = await fs.readdir(current, { withFileTypes: true })
for (const e of entries) {
if (IGNORE.has(e.name)) continue
lines.push(`${prefix}${e.isDirectory() ? '📁' : '📄'} ${e.name}`)
if (e.isDirectory()) {
await walk(path.join(current, e.name), depth + 1, prefix + ' ')
}
}
}

await walk(root, 1, '')
return lines.join('\n')
}

② 注册(registry.ts 的 registerBuiltins):

this.register('list_dir_tree', listDirTreeDefinition, listDirTreeHandler)

③ 提示词登记(planner.ts 工具列表加一行):

'- list_dir_tree: 树状列出目录结构',

④ 验证:问 Agent「这个工作区有什么文件?」——它应该优先选 list_dir_tree 而不是多次 file_read。

工具设计的经验法则​

法则原因
description 写「何时用」而非「是什么」模型靠它做调用决策
输出紧凑(树形文本优于逐文件 JSON)省 token,模型读得快
参数少而精,都有默认值降低模型出错率
复杂结果截断 + 提示「可用 file_read 深入」避免超大 tool 消息撑爆上下文
永远过一遍 validatePath / validateCommand第 10 章的安全底线

12.2 实战 2:接入 DeepSeek 与任意 OpenAI 兼容服务​

得益于 pi-ai 桥接层,接入新厂商基本零成本。

内置预设(agent-core/llm/deepseek-presets.ts)已包含 deepseek-flash / deepseek-v4-pro:

// 预设核心字段
{
id: 'deepseek-v4-pro',
provider: 'deepseek',
baseUrl: 'https://api.deepseek.com/v1',
// Key 由用户在设置页填入,存 ~/.openbudy/config.json
}

toPiModel 的转换逻辑(第 6 章)会自动处理:deepseek 在 pi-ai 官方目录中有收录 → 直接复用其 Model 定义并覆盖 baseUrl。

添加全新厂商(如 Moonshot Kimi)也不需要写代码——用户在 UI 的「模型管理」里填:

字段值
Providermoonshot(自定义)
Base URLhttps://api.moonshot.cn/v1
Model IDkimi-k2
API Key用户的 key

未收录厂商走 openai-completions 兜底构造——只要对方是 OpenAI 兼容接口就能用。

注意 supportsToolCalling:如果某模型不支持 Function Calling,标记为 false 后 loop 可以选择不传 tools(该模型只能做纯对话)。

12.3 实战 3:调教系统提示词​

agent-core/planner.ts 是 Agent 的「性格与规章」所在。三个改造方向:

方向 A:改变工作风格​

// 原:涉及 3 个以上文件时先简述计划再执行
// 改成更谨慎的 Agent:
'1. 任何文件修改前,必须先用 1-2 句话说明要改什么并等待用户确认',
'2. 每次最多修改一个文件,改完汇报再继续',

方向 B:注入领域知识​

'## 领域约定',
'- 本工作区是 Vue 3 + <script setup> 项目,组件用 PascalCase',
'- 样式一律用 UnoCSS 原子类,不要写 <style> 块',
'- 提交信息遵循 Conventional Commits',

方向 C:约束工具偏好​

'## 工具使用偏好',
'- 了解结构优先用 list_dir_tree,不要逐个 file_read',
'- 跑脚本用 shell_execute 时必须加超时:timeout 30 node xxx.js',
'- 禁止使用 npm install,依赖变更需用户手动执行',

改完重启 pnpm electron:dev 立即生效。提示词是软约束——它引导但不保证;硬约束必须放沙箱层(第 10 章)。

12.4 扩展点全景图​

想做的事改哪里难度
加工具agent-core/tools/ + registry + planner⭐
加模型预设agent-core/llm/*-presets.ts⭐
加 Agent 行为规则planner.ts 系统提示词⭐
加事件类型(如耗时统计)types + loop + useAgent(第 9 章实战)⭐⭐
换掉 pi-ai重写 llm/ 桥接层,保持 LLMChunk 协议⭐⭐
把 agent-core 移植到 CLI直接 import,Node 环境可跑⭐⭐
多窗口/多任务并发事件按 taskId 路由已支持;注意 abortControllers 是 Map 结构⭐⭐⭐

12.5 小结​

  • 加工具四步:definition → register → planner 登记 → 验证
  • 工具 description 是给模型的说明书,「何时用」比「是什么」重要
  • 新厂商接入几乎零代码:预设或 UI 自定义均可
  • 提示词管软约束(风格/偏好),沙箱管硬约束(安全)