503 lines
14 KiB
Markdown
503 lines
14 KiB
Markdown
# 自定义工具开发指南(非常详细|三层架构版)
|
||
|
||
适用对象:**从零开始、第一次上手** 的管理员。
|
||
当前能力:**仅支持 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.py`(Python 模板)
|
||
你提供“低代码逻辑”:
|
||
- 文件内容会先被做一次 **字符串格式化**(把 `{参数名}` 替换成实际入参)
|
||
- 然后在容器内作为 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 推荐字段(强烈建议都写)
|
||
```json
|
||
{
|
||
"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):执行超时秒数
|
||
|
||
> 注意:当前实现里 `required` 以 `parameters.required` 为准;另外顶层 `required` 字段也会被识别,但建议统一写在 `parameters.required`。
|
||
|
||
---
|
||
### 4.2 定义层示例 A:最小可用“回声工具”
|
||
文件:`data/custom_tools/echo/definition.json`
|
||
```json
|
||
{
|
||
"id": "echo",
|
||
"description": "回声:原样返回 text",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"text": { "type": "string", "description": "要回显的文本" }
|
||
},
|
||
"required": ["text"]
|
||
},
|
||
"category": "custom",
|
||
"timeout": 10
|
||
}
|
||
```
|
||
|
||
---
|
||
### 4.3 定义层示例 B:可选参数(optional)怎么写?
|
||
```json
|
||
{
|
||
"id": "greet",
|
||
"description": "打招呼:name 可选",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"name": { "type": "string", "description": "名字(可选)" }
|
||
},
|
||
"required": []
|
||
}
|
||
}
|
||
```
|
||
执行层里要记得处理 name 缺失的情况(见执行层示例)。
|
||
|
||
---
|
||
### 4.4 反例:参数 schema 写错导致模型不会传参
|
||
反例(`type` 写成 string):
|
||
```json
|
||
{
|
||
"id": "bad_schema",
|
||
"parameters": {
|
||
"type": "string",
|
||
"properties": {
|
||
"a": { "type": "number" }
|
||
}
|
||
}
|
||
}
|
||
```
|
||
后果:模型端可能无法正确构造对象参数,调用失败或入参为空。
|
||
|
||
---
|
||
## 5. 执行层 `execution.py`:核心坑点、写法规范、正反例
|
||
|
||
### 5.1 执行层“模板替换”规则(最关键)
|
||
执行层在运行前会做一次模板替换:
|
||
- `{a}` → 入参 a 的值
|
||
- `{op}` → 入参 op 的值
|
||
|
||
因此:
|
||
- **你写的不是纯 Python,而是 Python + 占位符**
|
||
- 任何你写的 `{...}` 都可能被当成占位符解析
|
||
|
||
---
|
||
### 5.2 大坑:字典/集合花括号必须双写 `{{` `}}`
|
||
|
||
你之前遇到的报错:
|
||
```json
|
||
{
|
||
"success": false,
|
||
"error": "缺少必填参数: \"'+'\""
|
||
}
|
||
```
|
||
根因就是模板引擎把这段当成“需要一个叫 `+'` 的占位符”:
|
||
```py
|
||
ops = {'+': operator.add, ...}
|
||
```
|
||
|
||
**正确写法:把字典的 `{}` 变成 `{{}}`**
|
||
示例(正确):
|
||
```py
|
||
ops = {{'+': operator.add, '-': operator.sub, '*': operator.mul, '/': operator.truediv}}
|
||
```
|
||
|
||
反例(错误):
|
||
```py
|
||
ops = {'+': operator.add}
|
||
```
|
||
|
||
同理:集合 `{1,2,3}`、字典推导式 `{k:v for ...}` 都需要转义,否则会被误当占位符。
|
||
|
||
---
|
||
### 5.3 参数是字符串时,必须自己加引号(或安全编码)
|
||
|
||
如果参数是字符串:
|
||
- `{name}` 替换后会变成裸文本,导致 Python 语法错误
|
||
|
||
正确(加引号):
|
||
```py
|
||
name = '{name}'
|
||
```
|
||
|
||
反例(没引号,容易 SyntaxError):
|
||
```py
|
||
name = {name}
|
||
```
|
||
|
||
更稳健的写法(推荐):用 JSON 序列化来安全注入字符串(避免引号/换行破坏代码)。
|
||
```py
|
||
import json
|
||
name = json.loads({name_json})
|
||
```
|
||
但这需要你在定义层把参数设计成 `name_json`,且调用时传入 JSON 字符串;新手不建议第一天就这么做。
|
||
|
||
---
|
||
### 5.4 强烈建议:执行层先做“输入校验”,再计算
|
||
因为没有强校验,入参可能是空、类型不对、字符串里有奇怪字符。
|
||
|
||
示例:对 calc 的 op 做严格白名单检查:
|
||
```py
|
||
a = {a}
|
||
op = '{op}'
|
||
b = {b}
|
||
if op not in ['+','-','*','/']:
|
||
raise ValueError('op must be one of + - * /')
|
||
```
|
||
|
||
反例(危险):用 eval 解析运算符/表达式
|
||
```py
|
||
print(eval(f\"{a}{op}{b}\")) # 不要这么写
|
||
```
|
||
原因:op 或其他参数可被注入恶意表达式。
|
||
|
||
---
|
||
### 5.5 执行层示例 1:echo(最简单)
|
||
文件:`data/custom_tools/echo/execution.py`
|
||
```py
|
||
text = '{text}'
|
||
print(text)
|
||
```
|
||
|
||
---
|
||
### 5.6 执行层示例 2:greet(可选参数)
|
||
文件:`data/custom_tools/greet/execution.py`
|
||
```py
|
||
name = '{name}'
|
||
name = (name or '').strip()
|
||
if not name:
|
||
print('你好!')
|
||
else:
|
||
print(f'你好,{name}!')
|
||
```
|
||
注意:这里用到了 f-string 的 `{name}`,**它是 Python 运行时的花括号,不会参与模板替换**,因为模板替换已经在执行前完成,且我们这里的 `{name}` 是在 Python 代码字符串里,不是模板语法(但仍要注意别在最外层写出新的 `{}` 占位符)。
|
||
|
||
---
|
||
### 5.7 执行层示例 3:calc(你要的三参数计算器)
|
||
文件:`data/custom_tools/calc/execution.py`
|
||
```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 反例:输出过大导致工具失败/截断
|
||
反例:
|
||
```py
|
||
print('A' * 1000000)
|
||
```
|
||
后果:输出有全局上限(默认 10k 字符左右),你会看到截断或失败提示。
|
||
建议:配合 return.json 的 `truncate`,或自行在执行层控制输出长度。
|
||
|
||
---
|
||
### 5.9 反例:死循环导致超时
|
||
```py
|
||
while True:
|
||
pass
|
||
```
|
||
后果:到 `timeout` 后被中断,返回 timeout。
|
||
|
||
---
|
||
## 6. 返回层 `return.json`:怎么做二次处理(例子 + 反例)
|
||
|
||
返回层是可选的,但强烈建议至少写一个 `truncate`,避免工具输出污染上下文。
|
||
|
||
### 6.1 支持字段(当前版本)
|
||
```json
|
||
{
|
||
"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`
|
||
```json
|
||
{
|
||
"truncate": 2000,
|
||
"template": "[calc] 结果: {output}"
|
||
}
|
||
```
|
||
|
||
---
|
||
### 6.3 反例:template 写错占位符导致忽略
|
||
```json
|
||
{ "template": "结果={result}" }
|
||
```
|
||
因为 `{result}` 不是支持变量,格式化会失败,系统会回退到默认 message(你会看到输出但不按模板展示)。
|
||
|
||
---
|
||
## 7. 备注/展示层 `meta.json`:备注与图标
|
||
|
||
### 7.1 meta.json 适合放什么?
|
||
示例:
|
||
```json
|
||
{
|
||
"notes": "这是一个示例工具,用于演示参数与模板写法",
|
||
"icon": "calculator.svg",
|
||
"owner": "admin",
|
||
"created_at": "2026-01-05"
|
||
}
|
||
```
|
||
|
||
### 7.2 图标怎么选?
|
||
目前图标字段会被保存/返回(前端后续可用它显示工具卡片 SVG)。
|
||
建议写成 `static/icons/` 下的文件名,例如:
|
||
- `brain.svg`
|
||
- `terminal.svg`
|
||
- `bot.svg`
|
||
|
||
你可以用命令查看有哪些图标:
|
||
```bash
|
||
ls static/icons
|
||
```
|
||
|
||
> 当前 UI 可能还没把 icon 真正渲染出来,但字段已经为未来对接准备好。
|
||
|
||
---
|
||
## 8. 如何创建一个新工具:从 0 到可用(完整示例)
|
||
|
||
下面示例创建一个工具 `text_len`:计算字符串长度,并在返回层截断。
|
||
|
||
### 8.1 创建目录
|
||
```
|
||
data/custom_tools/text_len/
|
||
```
|
||
|
||
### 8.2 definition.json
|
||
```json
|
||
{
|
||
"id": "text_len",
|
||
"description": "计算文本长度(字符数)",
|
||
"parameters": {
|
||
"type": "object",
|
||
"properties": {
|
||
"text": { "type": "string", "description": "输入文本" }
|
||
},
|
||
"required": ["text"]
|
||
},
|
||
"category": "custom",
|
||
"timeout": 10
|
||
}
|
||
```
|
||
|
||
### 8.3 execution.py
|
||
```py
|
||
text = '{text}'
|
||
print(len(text))
|
||
```
|
||
|
||
### 8.4 return.json(可选)
|
||
```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 推荐结构(字段越全越好):
|
||
```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 接入
|
||
|