数据截至 (上游 commit 3a4e2ae3eec0)
第 2 章 · 工具体系与权限引擎
这一章讲:工具怎么注册和分组、一次工具调用要过几道关、以及 AgentScope 怎 么判断一条 Bash 命令危不危险。
2.1 一次工具调用要过几道关
先看全景。_execute_tool_call(src/agentscope/agent/_agent.py:2238)是唯一入口,五道关:
模型给的 tool_call
│
① 可用性 toolkit.check_tool_available —— 工具存在吗?所在的组激活了吗?
│
② 入参 _json_loads_with_repair 修复畸形 JSON → jsonschema.validate 校验
│
③ 权限 _check_permission —— 过中间件链 → PermissionEngine 仲裁
│ ├─ ASK ──► 发确认事件,挂起(第 1 章)
│ └─ DENY ──► 写一条 DENIED 结果,继续
④ 执行 _acting → toolkit.call_tool —— 统一成 ToolChunk 流
│
⑤ 收尾 超长结果切分卸载 → 写回上下文 → 状态置 FINISHED
每一关失败都不会抛异常给开发者,而是把错误当成工具结果喂回给模型。这类异常有专门的类型 AgentOrientedException(面向 agent 的),与 DeveloperOrientedException(面向开发者的、要真抛出去的)区分开。两个类定义在 src/agentscope/exception/_base.py:5 与 :21;分流点是 call_tool 的兜底 except 段——只有开发者向的异常原样重抛,其余一律转成一条 ERROR 状态的工具结果。
依据:src/agentscope/tool/_toolkit.py:352-355。
except Exception as e:
# Raise the developer-oriented exception
if isinstance(e, DeveloperOrientedException):
raise e from None
2.2 Toolkit:工具组与「元工具」
三类东西,一个入口
Toolkit(src/agentscope/tool/_toolkit.py:66)统一管三种能力来源:
| 来源 | 是什么 | 怎么给模型 |
|---|---|---|
| 工具 | ToolBase 子类 | 直接进 tools schema |
| MCP 服务器 | MCPClient | 拉取远端工具列表,转成本地工具 |
| 技能(skill) | 一个目录:SKILL.md + 脚本 | 不进 tools,只在系统提示词里列名字和描述 |
技能这条路线值得单说。DEFAULT_SKILL_INSTRUCTION 模板(src/agentscope/tool/_toolkit.py:51-63)里写死了一句:
IMPORTANT: Skills are NOT tools, and you cannot call a skill directly.
模型要用技能,得先调 SkillViewer 工具把 SKILL.md 读出来,再照着里面的说明去用别的工具。这是典型的渐进式披露:技能的完整指令不常驻上下文,只在需要时按需加载。
工具组:让 agent 自己开关工具
工具太多会稀释模型注意力。ToolGroup(src/agentscope/tool/_tool_group.py:10)把工具分组,默认只激活 basic 组;其余组由模型调用内置元工具 ResetTools 自行激活。
Toolkit
├── basic 组(永远激活)── 构造函数里的 tools / mcps / skills
├── 组 A(描述:处理 Excel 相关任务)── 未激活
└── 组 B(描述:数据库查询) ── 未激活
▲
└── 模型调用 ResetTools(groups=["B"]) 后激活
组被激活时,组的 instructions 会随元工具的返回值一起注入(模板见 src/agentscope/tool/_toolkit.py:44-48)。所以「激活一组工具」同时也是「加载一段使用说明」。
没激活就调该组的工具会怎样?check_tool_available(:552)抛 ToolGroupInactiveError,错误信息直接告诉模型该先调哪个工具(:581-585)——错误信息本身就是给模型的指令。
统一成流
call_tool(:225)把工具的四种返回形态归一:
| 工具返回什么 | 处理 |
|---|---|
单个 ToolChunk | 直接 yield,累加 |
| 异步生成器 | 逐块 yield,逐块累加 |
| 同步生成器 | 同上 |
| 其它 | 抛 DeveloperOrientedException |
最后 finally 里必定 yield 一个完整的 ToolResponse(:386-388)。所以调用方永远能靠「最后 一个是 ToolResponse」判断结束——异常路径也不例外。
2.3 并发批次:哪些工具能一起跑
模型一次可能吐 5 个工具调用。全串行太慢,全并发会互相踩。
_batch_tool_calls(src/agentscope/agent/_agent.py:1903)按工具的 is_concurrency_safe 属性保序分段:
调用序列: Read Grep Write Edit Glob
安全性: safe safe unsafe unsafe safe
└──┬──┘ └───┬──┘ └┬┘
并发批次1 串行批次2 并发批次3
(一起跑) (一个个跑) (一起跑)
三条设计细节:
- 保序:批次之间严格按模型给出的顺序执行,不重排。写操作的先后语义得以保留。
- 未注册的工具 算「安全」(
:1769-1771),理由写在注释里:它根本跑不起来,不会有副作用。 - 串行批次里一旦遇到需要确认或被中断,立刻停下(
_execute_sequential_tool_calls,:1794),后面的不跑。
并发批次里的确认去重
5 个并发的 Read 全都要确认,弹 5 个框,用户会疯。kept_rules 就是解决这个的(:1905-1911 定义,:2206-2222 使用):
工人1 触发 ASK → 把它的「建议规则」记进 kept_rules → 弹框
工人2 触发 ASK → 先拿自己的入参去匹配 kept_rules
├─ 匹配上 → 不弹框,保持 PENDING,等下一轮重新评估
└─ 没匹配 → 记录并弹框
用户对第一个框点了「允许 src/** 」并加规则后,第二个调用下一轮直接被规则放行。
但安全性 ASK 不去重(:2202-2206