我们已经搞定了:
- 概念篇:把"Agent"和"会聊天的 LLM / RAG"区分开;
- 架构篇:用 5 个核心组件(意图理解 / 规划 / 执行 / 状态 / 评估)搭出一套通用 Agent 骨架。
这一篇,我们专门盯住其中最关键、也最容易踩坑的一层:
工具层(Tools / Actions)
------ 也就是「给 Agent 安装技能」的地方。
因为在工程实践里,Agent 之所以有价值,本质上不是"会说",而是:
- 能查业务数据(订单、工单、用户画像);
- 能调内部系统(CI/CD、告警、自愈、运维平台);
- 能把结论写回系统(发通知、建工单、改配置草案)。
这些"能做什么",全部都要通过受控的工具抽象暴露给 Agent。
一、为什么要专门设计"工具层",而不是随便调 API?
如果你直接在 Prompt 里写:
"你可以自由调用公司内部的任何 HTTP 接口......"
结果大概率会是:
- 安全风险:一不小心就调用了"删库 / 重启 / 改配置"这类高危操作;
- 行为不可控:你根本不知道它具体调了什么、参数是啥;
- 无法演进:每多一个接口就多一堆 Prompt 拼接逻辑,后期维护困难。
我自己的经验是:
要把"Agent 能干的事"收敛到一层统一的工具抽象里,让业务 API / 外部系统都变成一个个受控的 Tool。
可以用一句话概括这层设计目标:
所有可执行能力都必须通过 Tool 白名单显式暴露给 Agent,
不允许 Agent 直接碰真实世界的底层接口。
二、先设计一个通用的 Tool 抽象
2.1 基础接口定义
先用一个极简抽象,把"工具"统一成一个接口:
python
from abc import ABC, abstractmethod
from typing import Any, Dict
class Tool(ABC):
"""所有 Agent 工具的抽象基类"""
@abstractmethod
def name(self) -> str:
"""工具唯一标识,用于在规划和调用时引用"""
raise NotImplementedError
@abstractmethod
def description(self) -> str:
"""给 LLM/Agent 看的工具说明,写清楚能干嘛、不能干嘛"""
raise NotImplementedError
@abstractmethod
def input_schema(self) -> Dict[str, Any]:
"""输入参数约束(字段名、类型、是否必填、取值范围等)"""
raise NotImplementedError
@abstractmethod
def run(self, **kwargs) -> Any:
"""真正执行工具逻辑"""
raise NotImplementedError
有了这个抽象之后:
- 所有工具都有统一的"元信息"(name / description / 输入约束);
- 可以很方便生成「给大模型看的工具列表」;
- 也方便你在执行前做参数校验、权限控制和审计。
2.2 工具注册中心(Registry)
再实现一个 Registry,把所有 Tool 管理起来:
python
from typing import Dict, List, Optional
class ToolRegistry:
def __init__(self):
self._tools: Dict[str, Tool] = {}
def register(self, tool: Tool):
if tool.name() in self._tools:
raise ValueError(f"Tool {tool.name()} already registered")
self._tools[tool.name()] = tool
def get(self, name: str) -> Optional[Tool]:
return self._tools.get(name)
def list_tools(self) -> List[Tool]:
return list(self._tools.values())
def list_tool_specs_for_llm(self) -> List[Dict[str, Any]]:
"""给 LLM 用的工具描述(用于自动选工具)"""
specs = []
for t in self._tools.values():
specs.append({
"name": t.name(),
"description": t.description(),
"input_schema": t.input_schema()
})
return specs
之后 Agent 的"执行层 / 规划层"就不要直接依赖某个具体工具,而是依赖这个 ToolRegistry。
三、给 Agent 装上几类典型技能
下面我用一个"订单排障 Agent"的场景举例,给它装三类常见工具:
- 查询类(只读)
- 分析类(本地逻辑 / 子模型调用)
- 行动类(有副作用的操作)
3.1 查询类工具:GetOrderDetail
python
class GetOrderDetailTool(Tool):
def __init__(self, order_client):
self.order_client = order_client
def name(self) -> str:
return "get_order_detail"
def description(self) -> str:
return "根据 order_id 查询订单基础信息,仅支持只读操作。"
def input_schema(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"order_id": {"type": "string", "description": "订单ID"}
},
"required": ["order_id"]
}
def run(self, **kwargs) -> Dict[str, Any]:
order_id = kwargs["order_id"]
# 这里调用真实业务 API
data = self.order_client.get(order_id)
return {
"order_id": data.id,
"status": data.status,
"amount": data.amount,
"created_at": data.created_at.isoformat()
}
3.2 分析类工具:AnalyzePaymentError
python
class AnalyzePaymentErrorTool(Tool):
def name(self) -> str:
return "analyze_payment_error"
def description(self) -> str:
return "根据支付错误码和日志,给出简要的错误原因分类。"
def input_schema(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"error_code": {"type": "string"},
"logs": {"type": "array", "items": {"type": "string"}}
},
"required": ["error_code"]
}
def run(self, **kwargs) -> Dict[str, Any]:
code = kwargs["error_code"]
# 非LLM的业务逻辑示例,也可以在内部再次调用一个小模型
if code in ("CONN_TIMEOUT", "BANK_TIMEOUT"):
category = "NETWORK_OR_UPSTREAM"
elif code.startswith("CARD_"):
category = "USER_SIDE"
else:
category = "UNKNOWN"
return {
"category": category,
"explanation": f"支付错误可能属于 {category} 类问题"
}
3.3 行动类工具:CreateTicket(高危需限权)
python
class CreateTicketTool(Tool):
def __init__(self, ticket_client):
self.ticket_client = ticket_client
def name(self) -> str:
return "create_ticket"
def description(self) -> str:
return "创建一条运维/客服工单,用于后续人工跟进。(需权限检查)"
def input_schema(self) -> Dict[str, Any]:
return {
"type": "object",
"properties": {
"title": {"type": "string"},
"content": {"type": "string"},
"severity": {"type": "string", "enum": ["low", "medium", "high", "critical"]}
},
"required": ["title", "content"]
}
def run(self, **kwargs) -> Dict[str, Any]:
# 注意:这里在调用前应该经过上层的权限控制
ticket_id = self.ticket_client.create(
title=kwargs["title"],
content=kwargs["content"],
severity=kwargs.get("severity", "medium")
)
return {"ticket_id": ticket_id, "status": "created"}
注册工具:
python
registry = ToolRegistry()
registry.register(GetOrderDetailTool(order_client))
registry.register(AnalyzePaymentErrorTool())
registry.register(CreateTicketTool(ticket_client))
四、让模型"自动选工具":从自然语言到 Tool 调用
4.1 给 LLM 的工具列表提示(Tool Spec)
有了 ToolRegistry,你就可以在 Prompt 里给 LLM 一个结构化的"工具说明":
python
def build_tool_prompt(tools_for_llm: List[Dict[str, Any]]) -> str:
lines = ["你可以调用以下工具来完成任务:"]
for spec in tools_for_llm:
lines.append(f"- {spec['name']}: {spec['description']}")
lines.append(f" 参数: {json.dumps(spec['input_schema'], ensure_ascii=False)}")
lines.append("""
请根据当前任务,选一个最合适的工具,并给出调用参数。
只输出JSON,不要任何解释,格式如下:
{
"tool": "<工具名>",
"args": { ... }
}
""")
return "\n".join(lines)
在 Agent 运行中,你可以这么用:
python
tool_prompt = build_tool_prompt(registry.list_tool_specs_for_llm())
llm_input = f"""
【任务目标】
{goal}
【当前上下文】
{json.dumps(context, ensure_ascii=False)}
{tool_prompt}
"""
tool_call_json = llm(llm_input) # 调用大模型
tool_call = json.loads(tool_call_json)
tool_name = tool_call["tool"]
args = tool_call["args"]
然后交给执行器去跑。
4.2 参数安全校验
在真正调用前,一定要做参数校验,防止模型乱填:
python
from jsonschema import validate, ValidationError
def safe_execute_tool(registry: ToolRegistry, tool_name: str, raw_args: Dict[str, Any]) -> Dict[str, Any]:
tool = registry.get(tool_name)
if not tool:
return {"error": f"unknown_tool: {tool_name}"}
schema = tool.input_schema()
try:
validate(instance=raw_args, schema=schema)
except ValidationError as e:
return {"error": f"invalid_args: {e.message}"}
# 这里再接入权限检查
try:
result = tool.run(**raw_args)
return {"result": result}
except Exception as e:
return {"error": f"tool_execution_failed: {str(e)}"}
五、安全边界:不要让 Agent 变成"乱来"的超级管理员
工具层是最容易出事的地方,因此必须非常清晰地设计安全边界。
可以从三层来做防护:
5.1 工具白名单 + 分级
- 每个 Agent 实例都有一份"可用工具白名单";
- 对工具按风险分级:
- 只读:查询订单、查日志、查配置;
- 低风险写:创建工单、发通知、写审计日志;
- 高风险写:重启、扩容、改配置、删数据。
在 Tool 抽象上可以加一个 level:
python
class Tool(ABC):
@abstractmethod
def level(self) -> str:
"""
工具风险级别:
- read_only
- low_risk
- high_risk
"""
...
然后在执行器里按 level 决定是否允许自动执行、是否需要人工确认。
5.2 上下文约束 + 任务约束
- 在任务描述中加入
constraints:- 如 "只读操作"、 "不允许重启生产环境服务";
- 在 Tool 层读取这些约束,动态决定是否允许执行。
执行前加一层检查:
python
def check_constraints(task_constraints: List[str], tool: Tool) -> bool:
if "只读操作" in task_constraints and tool.level() != "read_only":
return False
# 可以继续加更多业务规则
return True
5.3 审计与回放
- 所有工具调用都必须带上:
- 谁发起(用户 / 系统);
- 哪个 Agent;
- 何时;
- 调了哪个 Tool、传了什么参数、结果如何。
- 这些记录既是安全审计,也是后续调试 Agent 行为的关键数据。
六、把工具层嵌回你的 Agent 架构里
结合你前一篇的架构(意图 → 规划 → 工具执行 → 状态 → 评估),现在我们可以把工具层正式放进去:
- 意图解析 :得到
task_type / goal / params / constraints; - 规划 :基于 task_type 生成一个或多个
PlanStep(每个包含action和args); - 执行 :
- 使用
ToolRegistry根据action查找 Tool; - 用
input_schema校验args; - 检查
constraints + level决定是否允许调用; - 调用
tool.run,把结果写回AgentState;
- 使用
- 评估:看这一步的结果是否足以完成目标,如果不够,再走下一步规划/执行。
在代码结构上,你可以把工具层单独一个 package:
python
agent_framework/
core/
intent_parser.py
planner.py
executor.py # 只依赖 ToolRegistry
state.py
evaluator.py
tools/
base.py # Tool 抽象
registry.py
order_tools.py
ticket_tools.py
...
七、落地建议:先从"只读工具"开始
如果你现在还没在生产上跑 Agent,或者还在 PoC 阶段,建议按这个顺序来:
-
只接只读工具
- get_xxx / list_xxx / describe_xxx
- 不做任何写操作,先绕开安全风险。
-
严格做参数校验
- 给每个工具写好
input_schema; - 执行前用 jsonschema 校验。
- 给每个工具写好
-
慢慢放开低风险写工具
- create_ticket / send_notification / write_audit_log;
- 一开始可以要求"需人工点击确认"才执行。
-
最后再接高风险写工具
- scale_service / restart_pod / change_config 等;
- 必须配合权限系统、变更流程和回滚策略。
八、小结
「给 Agent 安装技能」这一块拆开讲清楚了:
- 用统一的
Tool抽象 +ToolRegistry管理所有工具; - 把业务 API、分析逻辑、自愈动作都包装成受控工具;
- 通过工具列表 Prompt + LLM,让 Agent 能自动选工具;
- 用输入 schema + 约束检查 + 风险分级,给工具层筑起安全边界;
- 把工具层稳稳嵌入你上一篇的 5 层 Agent 架构中。