# 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 接口 ```typescript // registry.ts interface ToolExecutor { buildInputSchema(config: TConfig): JSONSchema7; // 返回给 LLM 的参数 schema buildDescription(config: TConfig): string; // 返回给 LLM 的工具描述 execute(input: unknown, config: TConfig, ctx: ToolContext): Promise; } interface ToolResult { success: boolean; data?: unknown; error?: string; metadata?: { statusCode?: number; responseSize?: number; durationMs: number }; } ``` ### 3.3 注册机制 ```typescript // index.ts 顶部立即注册 registerToolType("fetch", fetchExecutor); ``` **注意**:注册必须在 `index.ts` 中直接调用,不能依赖单独的 side-effect import 文件(Nitro tree-shaking 会移除无导出的 side-effect import)。 ### 3.4 Service 层核心函数 #### `executeAgentTool(id, input, userId)` 1. 查 DB 获取工具配置 2. 检查 enabled 3. `getExecutor(type)` 获取执行器(undefined 则返回失败) 4. 解析 config JSON 5. 调用 `executor.execute(input, config, ctx)` 6. 写日志 7. 返回 `ToolResult` #### `getEnabledToolsForLlm()` 查询所有 enabled 工具,转换为 ai-sdk `tool()` 格式: ```typescript 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-sdk `jsonSchema()` 包装 ### 3.5 fetch 执行器 #### 配置 (config.ts) ```typescript { 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) 1. zod 校验 input(url 必填,method/headers/body 可选) 2. SSRF 检查 `assertSafeUrl` 3. 域名白/黑名单检查 `checkDomainAccess` 4. 发起 fetch(AbortController 超时控制) 5. **HTTP 非 2xx 直接返回失败**(让模型知道请求无效) 6. 大小限制检查(content-length + 分块读取双重检查) 7. 按 parseMode 处理响应 8. 返回 `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` 参数: ```typescript 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 数组) ```typescript // 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 结束检测 ```typescript 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: - `reasoning` part:可折叠/展开,显示耗时 - `tool-call` part:显示工具名、参数、状态、结果 - `text` part:用 `marked` 增量渲染 Markdown(`v-html="renderMarkdown(part.text)"`) - **loading 动画**:三个跳动圆点 - **工具调用开关**:toggle,控制 `enableTools` 参数 ### 4.5 Markdown 渲染 ```typescript 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`:执行测试 Modal - `app/pages/admin/dashboard.vue`:已添加 Agent 工具管理入口 ### 4.7 composable 扩展 ```typescript // 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` 回调 ```typescript onError: (errorData: { error: unknown }) => void ``` 返回 `void`,不能返回 string。错误需通过日志或 stream error part 传递。 ### 6.3 `maxSteps` 语义 包含初始生成 + tool-call 轮次 + 最终回答。到上限后如果最后一步是 tool-call,`finishReason` 为 `tool-calls`,stream 直接结束,模型不会输出最终文本。 ### 6.4 `toDataStreamResponse` ```typescript result.toDataStreamResponse({ sendReasoning: true }) ``` `sendReasoning: true` 发送 reasoning part 到前端 stream。 ## 七、如何扩展新工具类型 ### 7.1 创建 executor ``` server/service/agent-tool/executors// ├── config.ts # 配置 zod schema + 默认值 ├── .ts # 执行器实现 └── (可选) 其他辅助文件 ``` executor 需实现 `ToolExecutor` 接口: ```typescript export const myExecutor: ToolExecutor = { buildInputSchema(config) { return { ... } }, buildDescription(config) { return "..." }, async execute(input, config, ctx) { return { success: true, data: ... } }, }; ``` ### 7.2 注册 在 `server/service/agent-tool/index.ts` 顶部添加: ```typescript import { myExecutor } from "./executors/my-type/my-type"; registerToolType("my-type", myExecutor); ``` ### 7.3 配置校验 在 `index.ts` 的 `validateConfig` 函数中添加新类型的校验: ```typescript function validateConfig(type: string, config: Record) { if (type === "fetch") return parseFetchConfig(config); if (type === "my-type") return parseMyConfig(config); return config; } ``` ### 7.4 input schema 在 `index.ts` 的 `getEnabledToolsForLlm` 中添加新类型的 zod schema: ```typescript 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` 中添加类型: ```typescript 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`,需 `as Record` 绕过 `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` | 本开发文档 |