You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

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_idcreated_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)

  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() 格式:

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 状态码和响应大小,帮助模型判断结果有效性
  • parametersz.toJSONSchema() 生成 JSON Schema,再用 ai-sdk jsonSchema() 包装

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)

  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 参数:

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,填充 resultstate: '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:
    • reasoning part:可折叠/展开,显示耗时
    • tool-call part:显示工具名、参数、状态、结果
    • text part:用 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:执行测试 Modal
  • app/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,finishReasontool-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.tsvalidateConfig 函数中添加新类型的校验:

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.tsgetEnabledToolsForLlm 中添加新类型的 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 本开发文档