agent-Specialization/doc/custom_tools_guide.md

503 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 自定义工具开发指南(非常详细|三层架构版)
适用对象:**从零开始、第一次上手** 的管理员。
当前能力:**仅支持 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 执行层示例 1echo最简单
文件:`data/custom_tools/echo/execution.py`
```py
text = '{text}'
print(text)
```
---
### 5.6 执行层示例 2greet可选参数
文件:`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 执行层示例 3calc你要的三参数计算器
文件:`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 接入