agent-Specialization/doc/custom_tools_guide.md

14 KiB
Raw Blame History

自定义工具开发指南(非常详细|三层架构版)

适用对象:从零开始、第一次上手 的管理员。
当前能力:仅支持 Python,且必须在容器内执行;自定义工具仅管理员可见/可用。

本项目默认不做严格校验(私有化客制化场景)。这让你更自由,但也更容易踩坑。本文用大量例子/反例把坑提前说明。


0. 快速上手5 分钟 checklist

  1. 创建工具文件夹:data/custom_tools/<tool_id>/
  2. 写 3 个文件:
    • definition.json(定义层)
    • execution.py(执行层)
    • return.json(返回层,可选但强烈建议先写一个简单截断)
  3. 刷新管理端:
    • 打开 admin/policy:工具应该出现在 custom 分类的可选列表里
  4. 用管理员账号在聊天中调用工具(让模型调用或你手动触发)

如果第 3 步看不到工具:先看本文的“常见问题/排查”章节。


1. 三层架构:你在写什么?

每个自定义工具由三层组成,各层之间通过“工具运行时”串起来:

1.1 定义层:definition.json

你告诉系统:

  • 工具 ID也是模型调用的函数名
  • 工具用途描述(给模型看,决定它会不会用、怎么用)
  • 参数 schema模型按它组织参数
  • 分类/图标/超时等元信息

1.2 执行层:execution.pyPython 模板)

你提供“低代码逻辑”:

  • 文件内容会先被做一次 字符串格式化(把 {参数名} 替换成实际入参)
  • 然后在容器内作为 Python 脚本运行

1.3 返回层:return.json(后处理)

对执行结果二次处理,例如:

  • 限制返回长度(避免输出爆炸)
  • 用模板把结果包装成更友好的消息

2. 目录结构规范(必须遵守)

每个工具一个独立目录:data/custom_tools/<tool_id>/

标准文件名:

definition.json   # 定义层(必需)
execution.py      # 执行层(必需)
return.json       # 返回层(可选)
meta.json         # 备注/展示层(可选)

只要某个目录同时存在 definition.json + execution.py,系统就会把它加载为“自定义工具”。

文件编码建议统一 UTF-8。


3. 运行时规则(你需要牢记的“真实行为”)

3.1 谁能看到/调用?

  • 只有管理员账号能看到/调用自定义工具
  • 普通用户看不到工具,也无法在聊天中调用

3.2 工具如何出现在 admin/policy

  • 后端会自动注入一个分类 custom(名称“自定义工具”)
  • 并把扫描到的自定义工具 ID 填入该分类的工具列表
  • 所以 admin/policy 的“工具可选列表”会出现这些工具名,从而可分配到别的分类

3.3 执行层为什么“像模板”?

执行层会先做一次 Python str.format 类似的格式化:

  • {a}{op}{b} 这样的占位符会被替换
  • 这会导致一个大坑:Python 字典/集合的花括号 {} 会被误认为占位符(下文详解)

4. 定义层 definition.json:字段规范 + 大量示例

4.1 推荐字段(强烈建议都写)

{
  "id": "calc",
  "description": "简单两数运算(加减乘除),仅管理员可见",
  "parameters": {
    "type": "object",
    "properties": {
      "a": { "type": "number", "description": "第一个数字" },
      "op": { "type": "string", "enum": ["+", "-", "*", "/"], "description": "运算符" },
      "b": { "type": "number", "description": "第二个数字" }
    },
    "required": ["a", "op", "b"]
  },
  "category": "custom",
  "icon": "brain.svg",
  "timeout": 15
}

字段说明:

  • id(必填):工具 ID建议与文件夹名一致例如文件夹 calc/id 也叫 calc
  • description(强烈建议):越清楚越好(模型会读它决定是否调用)
  • parameters建议JSON Schema 风格
    • type:固定 "object"
    • properties:参数字典
    • required:必填参数数组
  • category(可选,默认 custom用于管理端分类
  • icon可选SVG 文件名(来自 static/icons/
  • timeout(可选,默认 30执行超时秒数

注意:当前实现里 requiredparameters.required 为准;另外顶层 required 字段也会被识别,但建议统一写在 parameters.required


4.2 定义层示例 A最小可用“回声工具”

文件:data/custom_tools/echo/definition.json

{
  "id": "echo",
  "description": "回声:原样返回 text",
  "parameters": {
    "type": "object",
    "properties": {
      "text": { "type": "string", "description": "要回显的文本" }
    },
    "required": ["text"]
  },
  "category": "custom",
  "timeout": 10
}

4.3 定义层示例 B可选参数optional怎么写

{
  "id": "greet",
  "description": "打招呼name 可选",
  "parameters": {
    "type": "object",
    "properties": {
      "name": { "type": "string", "description": "名字(可选)" }
    },
    "required": []
  }
}

执行层里要记得处理 name 缺失的情况(见执行层示例)。


4.4 反例:参数 schema 写错导致模型不会传参

反例(type 写成 string

{
  "id": "bad_schema",
  "parameters": {
    "type": "string",
    "properties": {
      "a": { "type": "number" }
    }
  }
}

后果:模型端可能无法正确构造对象参数,调用失败或入参为空。


5. 执行层 execution.py:核心坑点、写法规范、正反例

5.1 执行层“模板替换”规则(最关键)

执行层在运行前会做一次模板替换:

  • {a} → 入参 a 的值
  • {op} → 入参 op 的值

因此:

  • 你写的不是纯 Python而是 Python + 占位符
  • 任何你写的 {...} 都可能被当成占位符解析

5.2 大坑:字典/集合花括号必须双写 {{ }}

你之前遇到的报错:

{
  "success": false,
  "error": "缺少必填参数: \"'+'\""
}

根因就是模板引擎把这段当成“需要一个叫 +' 的占位符”:

ops = {'+': operator.add, ...}

正确写法:把字典的 {} 变成 {{}}
示例(正确):

ops = {{'+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv}}

反例(错误):

ops = {'+': operator.add}

同理:集合 {1,2,3}、字典推导式 {k:v for ...} 都需要转义,否则会被误当占位符。


5.3 参数是字符串时,必须自己加引号(或安全编码)

如果参数是字符串:

  • {name} 替换后会变成裸文本,导致 Python 语法错误

正确(加引号):

name = '{name}'

反例(没引号,容易 SyntaxError

name = {name}

更稳健的写法(推荐):用 JSON 序列化来安全注入字符串(避免引号/换行破坏代码)。

import json
name = json.loads({name_json})

但这需要你在定义层把参数设计成 name_json,且调用时传入 JSON 字符串;新手不建议第一天就这么做。


5.4 强烈建议:执行层先做“输入校验”,再计算

因为没有强校验,入参可能是空、类型不对、字符串里有奇怪字符。

示例:对 calc 的 op 做严格白名单检查:

a = {a}
op = '{op}'
b = {b}
if op not in ['+','-','*','/']:
    raise ValueError('op must be one of + - * /')

反例(危险):用 eval 解析运算符/表达式

print(eval(f\"{a}{op}{b}\"))  # 不要这么写

原因op 或其他参数可被注入恶意表达式。


5.5 执行层示例 1echo最简单

文件:data/custom_tools/echo/execution.py

text = '{text}'
print(text)

5.6 执行层示例 2greet可选参数

文件:data/custom_tools/greet/execution.py

name = '{name}'
name = (name or '').strip()
if not name:
    print('你好!')
else:
    print(f'你好,{name}')

注意:这里用到了 f-string 的 {name}它是 Python 运行时的花括号,不会参与模板替换,因为模板替换已经在执行前完成,且我们这里的 {name} 是在 Python 代码字符串里,不是模板语法(但仍要注意别在最外层写出新的 {} 占位符)。


5.7 执行层示例 3calc你要的三参数计算器

文件:data/custom_tools/calc/execution.py

a = {a}
op = '{op}'
b = {b}
import operator
ops = {{'+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv}}
print(ops[op](a, b))

要点回顾:

  • 数字:a = {a}b = {b}
  • 字符串:op = '{op}'
  • 字典:ops = {{...}}(双花括号)

5.8 反例:输出过大导致工具失败/截断

反例:

print('A' * 1000000)

后果:输出有全局上限(默认 10k 字符左右),你会看到截断或失败提示。
建议:配合 return.json 的 truncate,或自行在执行层控制输出长度。


5.9 反例:死循环导致超时

while True:
    pass

后果:到 timeout 后被中断,返回 timeout。


6. 返回层 return.json:怎么做二次处理(例子 + 反例)

返回层是可选的,但强烈建议至少写一个 truncate,避免工具输出污染上下文。

6.1 支持字段(当前版本)

{
  "truncate": 2000,
  "template": "[tool_id={tool_id}] 输出: {output}"
}
  • truncate:整数,输出超过该长度时截断到前 N 字符
  • template:字符串模板,可用变量:
    • {output}stdout+stderr 合并后的输出文本(已按 truncate 处理后的版本)
    • {stderr}stderr如果存在
    • {return_code}:进程返回码
    • {tool_id}:工具 id

template 只是影响 message 展示,不会改变 output 字段本身(除 truncate


6.2 返回层示例calc 结果更友好

文件:data/custom_tools/calc/return.json

{
  "truncate": 2000,
  "template": "[calc] 结果: {output}"
}

6.3 反例template 写错占位符导致忽略

{ "template": "结果={result}" }

因为 {result} 不是支持变量,格式化会失败,系统会回退到默认 message你会看到输出但不按模板展示


7. 备注/展示层 meta.json:备注与图标

7.1 meta.json 适合放什么?

示例:

{
  "notes": "这是一个示例工具,用于演示参数与模板写法",
  "icon": "calculator.svg",
  "owner": "admin",
  "created_at": "2026-01-05"
}

7.2 图标怎么选?

目前图标字段会被保存/返回(前端后续可用它显示工具卡片 SVG
建议写成 static/icons/ 下的文件名,例如:

  • brain.svg
  • terminal.svg
  • bot.svg

你可以用命令查看有哪些图标:

ls static/icons

当前 UI 可能还没把 icon 真正渲染出来,但字段已经为未来对接准备好。


8. 如何创建一个新工具:从 0 到可用(完整示例)

下面示例创建一个工具 text_len:计算字符串长度,并在返回层截断。

8.1 创建目录

data/custom_tools/text_len/

8.2 definition.json

{
  "id": "text_len",
  "description": "计算文本长度(字符数)",
  "parameters": {
    "type": "object",
    "properties": {
      "text": { "type": "string", "description": "输入文本" }
    },
    "required": ["text"]
  },
  "category": "custom",
  "timeout": 10
}

8.3 execution.py

text = '{text}'
print(len(text))

8.4 return.json可选

{
  "truncate": 200,
  "template": "[text_len] 字符数: {output}"
}

8.5 验证

  1. 管理端 admin/policy:应看到 text_len
  2. 管理员聊天:让模型调用 text_len,或提示它“用 text_len 计算这段话长度”

9. 用 API 管理工具(可选)

如果你不想手工改文件,也可以调用后端接口(管理员权限):

9.1 列表

GET /api/admin/custom-tools

9.2 新增/更新

POST /api/admin/custom-tools

请求 JSON 推荐结构(字段越全越好):

{
  "id": "calc",
  "description": "简单两数运算",
  "parameters": { "type": "object", "properties": {}, "required": [] },
  "required": [],
  "category": "custom",
  "icon": "brain.svg",
  "timeout": 15,
  "execution_code": "print('hello')\n",
  "return": { "truncate": 2000, "template": "输出: {output}" },
  "meta": { "notes": "通过 API 创建" }
}

9.3 删除

DELETE /api/admin/custom-tools?id=calc


10. 常见问题与排查(非常实用)

10.1 admin/policy 看不到工具

逐条检查:

  1. 你是否用 管理员账号 登录?
  2. 工具目录是否存在:data/custom_tools/<tool_id>/
  3. 是否同时存在两个文件:
    • data/custom_tools/<tool_id>/definition.json
    • data/custom_tools/<tool_id>/execution.py
  4. definition.json 是否是合法 JSON多一个逗号都会解析失败
  5. 刷新页面(强刷),必要时重启后端服务

10.2 调用时报 “缺少必填参数: "'+'"” 或类似奇怪 key

几乎可以确定是:执行层里有未转义的字典/集合花括号
{ 改成 {{,把 } 改成 }}(只针对“字典/集合字面量部分”)。

10.3 调用时报 SyntaxError语法错误

常见原因:

  • 字符串参数没加引号:name = {name}(错误)
  • 参数中包含换行/引号导致代码被截断(建议先做简单输入,或改用更稳健的注入方式)

10.4 调用成功但输出很乱/太长

解决:

  • return.json 里加 truncate
  • 或执行层自行控制输出

10.5 工具能执行但模型不愿意用/总用错

解决:

  • 优化 description:写清楚输入输出、限制、例子
  • parameters.properties.*.description 写清楚含义
  • enum 尽量给明确可选值(例如运算符)

11. 规范建议(团队协作必看)

11.1 命名规范

  • 工具 id小写 + 下划线(例如 calc, text_len, db_query
  • 目录名与 definition.json.id 保持一致

11.2 输出规范

建议输出尽量“短而确定”:

  • 优先输出一个结果值或一段简短摘要
  • 需要结构化时输出 JSON但注意长度

11.3 安全与边界

虽然是私有化环境,也强烈建议:

  • 不要在执行层写破坏性命令/删除文件
  • 不要 eval 用户输入
  • 给工具合理的 timeout
  • 给返回层加 truncate

12. 当前版本限制(请先接受这些现实)

  • 仅支持 Python没有 NodeJS 执行)
  • 不提供强校验与沙箱策略自定义(依赖现有容器执行与全局限制)
  • 图标字段已预留(icon),但前端是否渲染取决于后续 UI 接入