前置阅读:一个最简化的 AI Agent 核心实现 核心模块:
SkillManager.py(115 行)+ 主循环集成代码
一、问题:工具多了怎么办?
基础 Agent 只有 search_web 和 calculate 两个工具,prompt 很小。但真实场景中,Agent 可能拥有几十上百个工具------文件读写、数据库查询、代码执行、邮件发送、日历管理......
直接把所有工具塞进 system prompt 会有什么后果?
| 问题 | 影响 |
|---|---|
| Token 浪费 | 每次 LLM 调用都附带全部工具定义,无关工具白白消耗 token |
| 注意力稀释 | tool_choice 需要在大量工具中选,准确率下降 |
| 幻觉工具调用 | LLM 可能调用不该用的工具 |
| 维护困难 | 改一个工具要改全局注册表 |
核心矛盾:Agent 需要很多能力,但每次对话只需要其中一小部分。
二、设计思路:工具 → Skill → 按需激活
scss
传统方案 渐进式方案
──────── ──────────
全部工具常驻 prompt 只发 load_skill / unload_skill
│ │
├─ search_web ├─ load_skill ← 元工具(始终可用)
├─ calculate ├─ unload_skill ← 元工具(始终可用)
├─ read_file │
├─ write_file └─ 按需激活 ──→ ┌ search
├─ query_db ├ calculator
├─ send_email ├ file-ops
├─ ... (N 个工具) └ ...
└─ 全部塞进 LLM 请求 每次只发当前激活的工具
核心思想:把"选工具"这件事也交给 Agent 自己 。LLM 通过 load_skill 和 unload_skill 两个元工具,自主决定什么时候加载 / 卸载哪些技能模块。
三、数据结构:Skill 是什么
python
@dataclass
class Skill:
"""一个 Skill = 一组工具 + 一段领域知识"""
name: str # 唯一标识,如 "calculator"
description: str # 给 LLM 看的简介,"数学计算能力"
tools: list[dict] # OpenAI tool 定义列表
tool_map: dict[str, Callable] # 工具名 → 实际函数
system_prompt: str = "" # 激活时追加到 system prompt
一个 Skill 打包了三个东西:
- 工具定义 (
tools)------ LLM 需要知道"有哪些工具、什么参数" - 工具实现 (
tool_map)------ Host 端执行的函数 - 领域知识 (
system_prompt)------ 这个技能该怎么用、有什么注意事项
示例:注册三个 Skill
python
skills = SkillManager()
skills.register(Skill(
name="search",
description="互联网搜索能力",
tools=[{"type": "function", "function": {"name": "search_web", ...}}],
tool_map={"search_web": search_web},
system_prompt="你拥有搜索能力。遇到不确定的事实性问题,请先搜索再回答。",
))
skills.register(Skill(
name="calculator",
description="数学计算能力",
tools=[{"type": "function", "function": {"name": "calculate", ...}}],
tool_map={"calculate": calculate},
system_prompt="你拥有计算能力。遇到数学计算请调用 calculate 工具,不要心算。",
))
skills.register(Skill(
name="file-ops",
description="文件读写能力",
tools=[
{"type": "function", "function": {"name": "read_file", ...}},
{"type": "function", "function": {"name": "write_file", ...}},
],
tool_map={"read_file": read_file, "write_file": write_file},
system_prompt="你拥有文件读写能力。操作文件前请确认路径正确。",
))
注册后,LLM 初始状态下看不到 search / calculator / file-ops 的具体工具,只能看到:
makefile
可用工具:
- load_skill → 描述里包含 "可用技能: search, calculator, file-ops"
- unload_skill
四、元工具:让 Agent 自己管理工具
4.1 元工具定义
python
META_TOOLS = [
{
"type": "function",
"function": {
"name": "load_skill",
"description": "加载一个技能模块。可用技能: {available_skills}",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "要加载的技能名称"}
},
"required": ["name"],
},
},
},
{
"type": "function",
"function": {
"name": "unload_skill",
"description": "卸载一个技能模块。当前已加载: {loaded_skills}",
"parameters": {
"type": "object",
"properties": {
"name": {"type": "string", "description": "要卸载的技能名称"}
},
"required": ["name"],
},
},
},
]
关键细节:{available_skills} 和 {loaded_skills} 是每次查询时动态填充的占位符,让 LLM 始终知道可选什么、已经加载了什么。
4.2 动态填充
python
def get_active_tools(self) -> list[dict]:
"""只返回当前激活 skill 的工具 + 元工具"""
tools = []
available = ", ".join(self._skills.keys())
loaded = ", ".join(self._active) if self._active else "无"
for mt in self.META_TOOLS:
tool_def = json.loads(json.dumps(mt))
tool_def["function"]["description"] = \
tool_def["function"]["description"].format(
available_skills=available, loaded_skills=loaded
)
tools.append(tool_def)
for name in self._active:
tools.extend(self._skills[name].tools)
return tools
五、主循环集成:每次调用前动态组装
在上一篇的基础 Agent 循环中,tools 是固定的。引入 Skill 后,每次 LLM 调用前都要动态组装:
python
for step in range(max_steps):
# ← 每轮动态获取当前激活的工具
active_tools = skills.get_active_tools()
active_tool_map = skills.get_active_tool_map()
# ← 动态拼装 system prompt(base + 激活 skill 的领域知识)
system_prompt = build_system_prompt(base_system_prompt, skills)
messages[0]["content"] = system_prompt
response = client.chat.completions.create(
model="your-model-name",
messages=messages,
tools=active_tools, # ← 不是全部 TOOLS,而是按需的
tool_choice="auto",
)
完整交互流程
ini
用户: "北京天气怎么样?顺便算 156*23"
Round 1:
tools = [load_skill, unload_skill]
LLM → load_skill("search") # 先加载搜索能力
Round 2:
tools = [load_skill, unload_skill, search_web]
LLM → search_web("北京天气") # 搜索
Round 3:
tools = [load_skill, unload_skill, search_web]
LLM → load_skill("calculator") # 需要计算,再加载
Round 4:
tools = [load_skill, unload_skill, search_web, calculate]
LLM → calculate("156*23") # 计算
Round 5:
LLM → 最终回答 # 整合结果输出
会话恢复时自动恢复 Skill 状态
python
if session_id:
state = pm.load_session(session_id)
for name in state.get("active_skills", []):
skills.load(name) # ← 恢复之前激活的 Skill
六、对比:无 Skill vs 有 Skill
| 维度 | 无 Skill(上一篇) | 有 Skill(本篇) |
|---|---|---|
| 工具数量 | 2 个,硬编码 | 任意多个,注册即用 |
| Prompt 大小 | 随工具数量线性增长 | 只包含当前激活的工具 |
| 工具选择 | LLM 在全部工具中选 | 先通过 load_skill 缩小范围 |
| 领域知识 | 写死在全局 system prompt | 每个 Skill 自带,按需激活 |
| 可扩展性 | 加工具要改主循环代码 | skills.register(Skill(...)) |
| 会话恢复 | 恢复 messages | 恢复 messages + active_skills |
| 工具发现 | LLM 只能看到定义 | LLM 通过 description 自主"浏览" |
七、设计精要
7.1 三个不变原则
- 主循环不变 ------ 仍是「思考 → 行动 → 观察」的 ReAct 循环,只是 tools 和 prompt 每轮动态生成
- 工具执行不变 ------
TOOL_MAP[name](**args)这行代码没有变,只是 map 的来源从全局变成了动态 - LLM 调用不变 ------
client.chat.completions.create的参数结构完全一致
7.2 "元工具"是高杠杆设计
两个 meta-tool(load_skill / unload_skill)让 Agent 获得了一种自省能力------它能意识到"我现在需要什么能力",然后主动获取。这个模式可以无限扩展:
list_skills→ 浏览所有可选技能search_skills→ 按关键词搜技能skill_status→ 查看当前技能树
7.3 为什么不用 MCP 动态发现
MCP(Model Context Protocol)的动态 tool discovery 需要在每次调用时拉取远端工具列表,而本方案是本地注册 + 按需激活:
| 本方案 | MCP dynamic | |
|---|---|---|
| 延迟 | 0(本地 dict 查询) | 网络往返 |
| 上下文控制 | 精确到单个 tool 级别 | tools/list 返回全部 |
| 领域知识 | Skill.system_prompt 随激活注入 | 需另行处理 |
7.4 渐进式 Skill 的正确心态
渐进式不等于"先写个简单的,以后慢慢加"。它指的是:
- 用户侧:agent 的能力随对话进展逐步解锁,不是开场就 show hand
- 开发者侧:新能力以 Skill 为单位注册,不碰主循环代码
- LLM 侧:每次只看到当前需要的工具子集,注意力不被稀释
八、完整架构图
全部代码增量:SkillManager 115 行 + MiniAgent 集成 ~20 行 + build_system_prompt 7 行 = < 150 行净增。
推荐学习资源
- 从零开始打造一个AI Agent CLI --- 手把手带你构建一个完整的 AI Agent 命令行工具,从工具调用、对话管理到发布上线的全流程实战。
- AI Agents 开发实践 --- 涵盖 Agent 架构设计、记忆系统、多 Agent 协作等核心主题的实战教程,适合想深入 Agent 开发的工程师。
- AI 全栈编程生存指南 --- AI 时代的全栈开发者生存法则,从 AI 辅助编程到底层原理,帮你建立不可替代的技术壁垒。