21 KiB
Agent 工具调用框架 - 开发文档
本文档供后续开发者快速了解工具调用框架的完整实现,便于扩展新工具类型和维护现有功能。
一、整体架构
┌──────────────────────────────────────────────────────┐
│ 前端 │
│ ┌───────────────┐ ┌──────────────────────────┐ │
│ │ 工具管理页面 │ │ LLM 测试页面 │ │
│ │ admin/ │ │ + 工具调用开关 │ │
│ │ agent-tools │ │ + agent 链路展示 │ │
│ └──────┬────────┘ └──────────┬───────────────┘ │
└─────────┼────────────────────────┼───────────────────┘
│ │
┌─────────┼────────────────────────┼───────────────────┐
│ 服务端 │ │ │
│ ┌──────▼────────┐ ┌──────────▼───────────────┐ │
│ │ agent-tools │ │ llm/chat API(扩展) │ │
│ │ API (CRUD+ │ │ tool-calling loop │ │
│ │ execute) │ │ maxSteps=8 │ │
│ └──────┬────────┘ └──────────┬───────────────┘ │
│ │ │ │
│ ┌──────▼────────────────────────▼───────────────┐ │
│ │ Tool Registry(工具注册中心) │ │
│ │ registerToolType(type, executor) │ │
│ │ getExecutor(type) → ToolExecutor │ │
│ └──────┬────────────────────────────────────────┘ │
│ │ │
│ ┌──────▼────────┐ ┌──────────────────────┐ │
│ │ agent_tools │ │ agent_tool_logs │ │
│ │ (DB 表) │ │ (DB 表) │ │
│ └───────────────┘ └──────────────────────┘ │
└──────────────────────────────────────────────────────┘
核心分层:
- Tool Registry:type → executor 的映射,硬编码执行逻辑 + 根据 config 差异化配置
- agent_tools 表:存工具实例(全局共享),含 type/config/enabled
- agent_tool_logs 表:执行日志
- agent-tools API:CRUD +
POST /agent-tools/:id/execute(独立执行,调试用) - llm/chat 扩展:注入 enabled 工具到 ai-sdk
streamText,自动 tool loop
二、数据库
2.1 agent_tools 表
| 字段 | 类型 | 说明 |
|---|---|---|
| id | text PK | at_{timestamp36}_{random} 格式 |
| name | text(50) | 工具显示名称 |
| slug | text(50) | 唯一标识,用于 LLM tool name |
| description | text | 工具描述 |
| type | text(30) | 工具类型(如 fetch) |
| config | text | JSON 字符串,工具配置 |
| enabled | integer | 0/1,是否启用 |
| sort_order | integer | 排序权重 |
| created_at | integer(ts_ms) | 创建时间 |
| updated_at | integer(ts_ms) | 更新时间 |
索引:slug 唯一索引、enabled 普通索引。
2.2 agent_tool_logs 表
| 字段 | 类型 | 说明 |
|---|---|---|
| id | integer PK auto | 自增 |
| tool_id | text | 关联 agent_tools.id |
| tool_slug | text(50) | 冗余存储 slug |
| user_id | integer | 用户 ID(可为 null) |
| input | text | 输入参数 JSON |
| output | text | 输出结果(截断 10000 字符) |
| status | text(20) | success / error / timeout |
| error_message | text | 错误信息 |
| duration_ms | integer | 执行耗时 |
| created_at | integer(ts_ms) | 创建时间 |
索引:tool_id、created_at。
2.3 迁移文件
packages/drizzle-pkg/migrations/0014_breezy_maestro.sql
三、服务端实现
3.1 目录结构
server/service/agent-tool/
├── registry.ts # ToolExecutor 接口 + 注册机制
├── index.ts # Service 层:CRUD + execute + getEnabledToolsForLlm
├── log.ts # 日志写入
└── executors/
└── fetch/
├── config.ts # fetch 配置 zod schema + 默认值
├── security.ts # SSRF 防护 + 域名白/黑名单
├── parse.ts # raw/markdown/json 解析
└── fetch.ts # fetch 执行器组装
3.2 ToolExecutor 接口
// registry.ts
interface ToolExecutor<TConfig> {
buildInputSchema(config: TConfig): JSONSchema7; // 返回给 LLM 的参数 schema
buildDescription(config: TConfig): string; // 返回给 LLM 的工具描述
execute(input: unknown, config: TConfig, ctx: ToolContext): Promise<ToolResult>;
}
interface ToolResult {
success: boolean;
data?: unknown;
error?: string;
metadata?: { statusCode?: number; responseSize?: number; durationMs: number };
}
3.3 注册机制
// index.ts 顶部立即注册
registerToolType("fetch", fetchExecutor);
注意:注册必须在 index.ts 中直接调用,不能依赖单独的 side-effect import 文件(Nitro tree-shaking 会移除无导出的 side-effect import)。
3.4 Service 层核心函数
executeAgentTool(id, input, userId)
- 查 DB 获取工具配置
- 检查 enabled
getExecutor(type)获取执行器(undefined 则返回失败)- 解析 config JSON
- 调用
executor.execute(input, config, ctx) - 写日志
- 返回
ToolResult
getEnabledToolsForLlm()
查询所有 enabled 工具,转换为 ai-sdk tool() 格式:
result[agentTool.slug] = tool({
description: executor.buildDescription(config),
parameters: jsonSchema(jsonSch, { validate: ... }),
execute: async (input) => {
const execResult = await executeAgentTool(agentTool.id, input, null);
if (!execResult.success) {
return `工具执行失败: ${execResult.error}。请停止调用此工具...`;
}
// 成功时附带元信息,帮助模型判断结果是否有效
return `[fetch 结果 HTTP ${statusCode} ${size} bytes]\n${data}`;
},
});
关键设计:
- 工具失败时返回明确的错误文本(不是 JSON 对象),引导模型停止重试
- 工具成功时附带 HTTP 状态码和响应大小,帮助模型判断结果有效性
parameters用z.toJSONSchema()生成 JSON Schema,再用 ai-sdkjsonSchema()包装
3.5 fetch 执行器
配置 (config.ts)
{
defaultMethod: "GET", // 默认 HTTP 方法
defaultHeaders: {}, // 默认请求头
timeout: 10000, // 超时 ms(最大 60000)
maxResponseSize: 102400, // 最大响应字节(最大 1MB)
allowedDomains: ["*"], // 域名白名单(空数组=允许所有)
blockedDomains: [], // 域名黑名单
parseMode: "markdown", // raw / markdown / json
}
安全 (security.ts)
- SSRF 防护:DNS 解析后检查所有 IP 是否为内网地址
- IPv4 私有范围:10.0.0.0/8、172.16.0.0/12、192.168.0.0/16、127.0.0.0/8、169.254.0.0/16、0.0.0.0/8、100.64.0.0/10
- IPv6:::1、fc00::/7、fe80::/10、::ffff: 映射的 IPv4
- 域名白名单:空数组时允许所有域名;支持通配符
*.example.com - 域名黑名单:优先于白名单
- 协议限制:仅 http/https
解析 (parse.ts)
raw:返回原始文本markdown:HTML → Markdown(用turndown@7.2.0),非 HTML 返回原文json:JSON.parse,失败则降级返回原文 +degraded: true
执行流程 (fetch.ts)
- zod 校验 input(url 必填,method/headers/body 可选)
- SSRF 检查
assertSafeUrl - 域名白/黑名单检查
checkDomainAccess - 发起 fetch(AbortController 超时控制)
- HTTP 非 2xx 直接返回失败(让模型知道请求无效)
- 大小限制检查(content-length + 分块读取双重检查)
- 按 parseMode 处理响应
- 返回
ToolResult
3.6 API 端点
| 方法 | 路径 | 权限 | 说明 |
|---|---|---|---|
| GET | /api/agent-tools |
admin | 列出所有工具 |
| POST | /api/agent-tools |
admin | 创建工具 |
| GET | /api/agent-tools/:id |
admin | 获取工具详情 |
| PUT | /api/agent-tools/:id |
admin | 更新工具 |
| DELETE | /api/agent-tools/:id |
admin | 删除工具 |
| POST | /api/agent-tools/:id/execute |
admin | 独立执行工具(调试用) |
所有端点使用 requireAdmin 权限校验。
3.7 chat 集成
server/api/llm/chat/index.post.ts 扩展了 enableTools 参数:
const tools = enableTools ? await getEnabledToolsForLlm() : undefined;
const result = streamText({
model: languageModel,
messages,
...(tools && Object.keys(tools).length > 0
? { tools, maxSteps: 8 }
: {}),
onError: (errorData) => {
logger.error(...);
},
onFinish: ({ finishReason, usage, steps }) => {
logger.info("finished: reason=%s steps=%d ...", finishReason, steps.length, ...);
},
});
return result.toDataStreamResponse({ sendReasoning: true });
关键参数:
maxSteps: 8:工具调用最大轮次(包含初始生成 + tool-call 轮次 + 最终回答)sendReasoning: true:发送 reasoning part 到前端onError:返回 void(ai-sdk v4 要求),错误打到日志onFinish:记录 finishReason 和步数,用于调试
四、前端实现
4.1 消息模型(parts 数组)
// app/composables/useLlmChat.ts
interface MessagePart {
id: string
type: 'text' | 'reasoning' | 'tool-call' | 'tool-result'
text?: string // text / reasoning part 的文本
toolName?: string // tool-call part 的工具名
toolCallId?: string // tool-call part 的调用 ID
args?: unknown // tool-call part 的参数
result?: unknown // tool-call part 的结果(tool-result 填充)
state?: 'call' | 'result' // tool-call part 的状态
reasoningLoading?: boolean // reasoning part 是否正在加载
reasoningDuration?: number // reasoning part 的耗时
}
interface LlmChatMessage {
id: string
role: 'user' | 'assistant'
content: string // 兼容字段,text part 的累加
parts?: MessagePart[] // 按 stream 到达顺序追加
}
4.2 stream 处理
processDataStream 的回调处理:
| 回调 | 处理逻辑 |
|---|---|
onReasoningPart |
追加到上一个 reasoning part(合并连续 reasoning),标记 reasoningLoading: true |
onTextPart |
先 updateLastReasoningDuration 标记 reasoning 完成,再追加到上一个 text part(合并连续 text) |
onToolCallPart |
先 updateLastReasoningDuration,再追加新的 tool-call part(state: 'call')。字段名是 args 不是 input |
onToolResultPart |
找到对应 toolCallId 的 tool-call part,填充 result 和 state: 'result'。字段名是 result 不是 output |
onErrorPart |
设置 errorMessage |
合并逻辑:getOrCreateLastPart(msg, type) 检查最后一个 part 是否同类型,是则返回它继续追加,否则返回 null 触发新建。
4.3 stream 结束检测
const hasText = finalMsg.parts?.some(p => p.type === 'text' && p.text)
const hasToolCall = finalMsg.parts?.some(p => p.type === 'tool-call')
if (!hasText && !hasToolCall) {
// 完全无内容,移除空消息
errorMessage.value = '模型未返回任何内容...'
messages.value.splice(assistantIdx, 1)
} else if (!hasText && hasToolCall) {
// 有工具调用但无最终文本(maxSteps 用完),保留工具记录,追加提示
finalMsg.parts?.push({
type: 'text',
text: '(已达到工具调用次数上限,模型未能生成最终回答...)',
})
}
4.4 LLM 测试页面渲染
app/pages/settings/llm-test/index.vue:
- user 消息:用
ChatBubble组件(右侧气泡) - assistant 消息:自定义
assistant-panel(左侧头像 + 右侧卡片容器),内部按序渲染 parts:reasoningpart:可折叠/展开,显示耗时tool-callpart:显示工具名、参数、状态、结果textpart:用marked增量渲染 Markdown(v-html="renderMarkdown(part.text)")
- loading 动画:三个跳动圆点
- 工具调用开关:toggle,控制
enableTools参数
4.5 Markdown 渲染
import { marked } from 'marked'
marked.setOptions({ breaks: true, gfm: true })
function renderMarkdown(text: string): string {
if (!text) return ''
try {
return marked.parse(text, { async: false }) as string
} catch {
return text
}
}
样式通过 .assistant-text :deep(...) 覆盖 markdown 元素样式(p、pre、code、ul、ol、blockquote、table 等)。
4.6 工具管理页面
app/pages/admin/agent-tools/index.vue:列表页,显示所有工具app/components/AgentToolFormModal.vue:创建/编辑表单 Modal- Input Schema 只读展示(fetch 工具参数固定)
- config 可编辑(JSON textarea)
app/components/AgentToolExecuteModal.vue:执行测试 Modalapp/pages/admin/dashboard.vue:已添加 Agent 工具管理入口
4.7 composable 扩展
// app/composables/useLlmChat.ts
export interface UseLlmChatOptions {
modelId: () => number | null
apiEndpoint?: string
systemPrompt?: () => string
enableThinking?: () => boolean
enableTools?: () => boolean // 新增
}
五、关键依赖
| 依赖 | 版本 | 用途 |
|---|---|---|
ai |
4.3.16 | ai-sdk,streamText / tool / processDataStream / jsonSchema |
@ai-sdk/openai-compatible |
- | OpenAI 兼容 provider |
zod |
4.3.6 | schema 校验,z.toJSONSchema() 生成 JSON Schema |
turndown |
7.2.0 | HTML → Markdown 转换 |
marked |
12.0.2 | Markdown → HTML 渲染(前端) |
注意:
zod-to-json-schema@3.x不兼容 zod v4,改用z.toJSONSchema()json-schema-to-zod返回的是代码字符串不是 zod 实例,已移除- ai-sdk
tool()的parameters接受jsonSchema()包装的对象或 zod schema
六、ai-sdk v4 关键约定
6.1 stream part 字段名
| stream part 类型 | 字段 | 说明 |
|---|---|---|
tool_call |
toolName, toolCallId, args |
不是 input |
tool_result |
toolCallId, result |
不是 output |
6.2 onError 回调
onError: (errorData: { error: unknown }) => void
返回 void,不能返回 string。错误需通过日志或 stream error part 传递。
6.3 maxSteps 语义
包含初始生成 + tool-call 轮次 + 最终回答。到上限后如果最后一步是 tool-call,finishReason 为 tool-calls,stream 直接结束,模型不会输出最终文本。
6.4 toDataStreamResponse
result.toDataStreamResponse({ sendReasoning: true })
sendReasoning: true 发送 reasoning part 到前端 stream。
七、如何扩展新工具类型
7.1 创建 executor
server/service/agent-tool/executors/<type>/
├── config.ts # 配置 zod schema + 默认值
├── <type>.ts # 执行器实现
└── (可选) 其他辅助文件
executor 需实现 ToolExecutor<TConfig> 接口:
export const myExecutor: ToolExecutor<MyConfig> = {
buildInputSchema(config) { return { ... } },
buildDescription(config) { return "..." },
async execute(input, config, ctx) { return { success: true, data: ... } },
};
7.2 注册
在 server/service/agent-tool/index.ts 顶部添加:
import { myExecutor } from "./executors/my-type/my-type";
registerToolType("my-type", myExecutor);
7.3 配置校验
在 index.ts 的 validateConfig 函数中添加新类型的校验:
function validateConfig(type: string, config: Record<string, unknown>) {
if (type === "fetch") return parseFetchConfig(config);
if (type === "my-type") return parseMyConfig(config);
return config;
}
7.4 input schema
在 index.ts 的 getEnabledToolsForLlm 中添加新类型的 zod schema:
const zodSchema = agentTool.type === "fetch"
? FETCH_INPUT_SCHEMA
: agentTool.type === "my-type"
? MY_INPUT_SCHEMA
: z.object({});
7.5 更新 ENUM
在 packages/drizzle-pkg/lib/schema/agent-tool.ts 中添加类型:
export const AgentToolTypes = ["fetch", "my-type"] as const;
7.6 前端
AgentToolFormModal.vue:添加新类型的 config 表单字段AgentToolFormModal.vue:Input Schema 只读展示新类型的参数
八、已知问题和注意事项
8.1 工具调用循环
模型可能反复调用工具但每次都失败(如 URL 无效),消耗完 maxSteps 后 stream 结束。已通过以下方式缓解:
- HTTP 非 2xx 返回
success: false+ 明确错误文本 - 工具失败时返回引导模型停止的提示文本
- 工具成功时附带元信息帮助模型判断结果有效性
maxSteps: 8给模型留余地
8.2 前端 execute 请求
AgentToolExecuteModal.vue 的 execute 请求可能 Failed to fetch,原因是 $fetch 未带 cookie 导致 401 或请求被浏览器拦截。需确保请求携带认证信息。
8.3 项目已有 bug
login_post$1 / renderer before initialization 错误(非本框架引入),可能导致无法通过 API 登录做完整端到端测试。
8.4 Nitro tree-shaking
不要依赖 side-effect import 注册工具(如 import "./register.ts"),Nitro 会移除无导出的 side-effect import。注册逻辑必须直接在 index.ts 中调用 registerToolType。
8.5 zod v4 兼容性
zod-to-json-schema@3.x不兼容 zod v4,用z.toJSONSchema()替代json-schema-to-zod返回代码字符串,不是 zod 实例,已移除- ai-sdk
jsonSchema()包装器接受Record<string, unknown>,需as Record<string, unknown>绕过JSONSchema7类型不匹配
8.6 BigInt 字面量
security.ts 中 BigInt 字面量(如 0x0a000000n)在某些 TypeScript 配置下有警告,改用 BigInt("0x0a000000") 调用形式。
九、文件清单
服务端
| 文件 | 说明 |
|---|---|
packages/drizzle-pkg/lib/schema/agent-tool.ts |
DB schema 定义 |
packages/drizzle-pkg/migrations/0014_breezy_maestro.sql |
迁移文件 |
server/service/agent-tool/registry.ts |
ToolExecutor 接口 + 注册机制 |
server/service/agent-tool/index.ts |
Service 层 CRUD + execute + getEnabledToolsForLlm |
server/service/agent-tool/log.ts |
日志写入服务 |
server/service/agent-tool/executors/fetch/config.ts |
fetch 配置 zod schema |
server/service/agent-tool/executors/fetch/security.ts |
SSRF 防护 + 域名检查 |
server/service/agent-tool/executors/fetch/parse.ts |
raw/markdown/json 解析 |
server/service/agent-tool/executors/fetch/fetch.ts |
fetch 执行器 |
server/api/agent-tools/index.get.ts |
列出工具 |
server/api/agent-tools/index.post.ts |
创建工具 |
server/api/agent-tools/[id].get.ts |
获取详情 |
server/api/agent-tools/[id].put.ts |
更新工具 |
server/api/agent-tools/[id].delete.ts |
删除工具 |
server/api/agent-tools/[id]/execute.post.ts |
独立执行 |
server/api/llm/chat/index.post.ts |
流式对话(扩展 tool-calling) |
前端
| 文件 | 说明 |
|---|---|
app/composables/useLlmChat.ts |
chat composable(parts 数组模型) |
app/pages/settings/llm-test/index.vue |
LLM 测试页面(agent 链路展示 + Markdown 渲染) |
app/pages/admin/agent-tools/index.vue |
工具管理列表页 |
app/pages/admin/dashboard.vue |
admin 仪表盘(含工具管理入口) |
app/components/AgentToolFormModal.vue |
工具创建/编辑表单 |
app/components/AgentToolExecuteModal.vue |
工具执行测试 |
文档
| 文件 | 说明 |
|---|---|
docs/superpowers/specs/2026-08-05-agent-tool-framework-design.md |
设计文档 |
docs/agent-tool-framework.md |
本开发文档 |