🦋 给 Jarvis 一双手:从'只会说话'到'能做事'的 Tool System 开发记录

🦋 给 Jarvis 一双手:从'只会说话'到'能做事'的 Tool System 开发记录

一、那个让我起鸡皮疙瘩的瞬间

你: 帮我创建一个叫 daily-standup 的 skill

Jarvis: 好的!我来为你创建一个 daily-standup skill 📝
  🔧 create_skill({name: 'daily-standup', ...}) ✅
✅ daily-standup skill 创建成功!
已保存到 ~/.jarvis/skills/daily-standup/SKILL.md

当我在终端看到这段输出时,愣了足足三秒。

Jarvis——我从零开始打造的 AI Agent——自己调用了工具,创建了一个新的技能文件。不是我写代码帮它做的,是它通过 LLM 的 function calling,自主决定调用 create_skill 工具,传入了正确的参数,然后在磁盘上生成了一个格式完美的 SKILL.md。

这就是 Phase 3 的终极目标:让 Jarvis 从"只能说话"进化到"能做事"。

而且不只是做事,是做创造性的事——它能创建新工具、新技能、甚至新的 MCP Server 骨架。这意味着,它已经拥有了自我扩展的基础设施。


二、灵感:构造实现 Tool 的 Tool

Phase 3 的设计灵感来自一次对话中的顿悟。

当时我在思考:该给 Jarvis 什么工具?文件操作?Shell 命令?HTTP 请求?这些都很基础,但感觉缺了什么。

然后一句话点醒了我:

"不是给它具体的工具,而是给它造工具的工具。"

这就像编程语言的层次:

  • 汇编是 Layer 0,直接操作硬件
  • 编译器是 Layer 1,用汇编构造高级语言
  • 应用程序是 Layer 2,用高级语言写的

类比到 Jarvis 的工具系统:

Layer 0 (Atomic)  — 不可再分的原始能力
  file_read, file_write, shell_exec, http_request

Layer 1 (Meta)    — 用 Layer 0 构造新能力
  create_skill, create_tool, create_mcp

Layer 2 (Emergent)— Jarvis 自己创造的(Phase 4 的目标)
  blog-writer, translator, daily-standup ...

四个原子 + 三个元工具 = 无限可能。 这就是整个架构的精髓。


三、技术实现:七个工具的诞生

3.1 Tool 基础框架

每个工具都是一个 Python 类,继承自 Tool 抽象基类:

class Tool(ABC):
    @property
    @abstractmethod
    def name(self) -> str: ...           # 工具名称

    @property
    @abstractmethod
    def description(self) -> str: ...    # 给 LLM 看的描述

    @property
    @abstractmethod
    def parameters(self) -> dict: ...    # JSON Schema 参数定义

    @abstractmethod
    async def execute(self, **kwargs) -> ToolResult: ...  # 执行

ToolRegistry 负责自动发现和管理工具——扫描 builtins/ 和 meta/ 目录,找到所有 Tool 子类,实例化并注册。零配置,加一个 .py 文件就自动可用。

3.2 Layer 0:四个原子

工具能力安全机制
file_read读取文件,支持行范围UTF-8 + latin-1 fallback,100K 截断
file_write创建/覆盖/追加文件系统路径阻止列表
shell_exec执行 Shell 命令危险命令拦截 + 超时保护
http_requestHTTP GET/POST/PUT/DELETEURL 协议验证

安全是我花了最多心思的部分。shell_exec 内置了危险命令检测:

_DANGEROUS_PATTERNS = (
    "rm -rf /", "rm -rf ~", "sudo rm",
    "mkfs", "dd if=", "> /dev/",
    "chmod 777 /", ":(){ :|:& };:",
)

file_write 则有路径保护:

_BLOCKED_PREFIXES = (
    "/System", "/usr", "/bin", "/sbin",
    "/etc", "/var/root", "/private/var/root",
)

3.3 Layer 1:三个元工具

这三个才是 Phase 3 最具想象力的部分:

  • create_skill — 生成 ~/.jarvis/skills/{name}/SKILL.md,带 YAML frontmatter + Markdown 指令 + 可选脚本
  • create_tool — 生成一个 Tool 子类的 Python 文件,可以动态加载
  • create_mcp — 生成完整的 MCP Server 项目骨架(pyproject.toml + FastMCP server.py)

它们的存在意味着:Jarvis 可以自己扩展自己的能力边界。

3.4 LLM Function Calling 集成

JarvisLLMClient 是整个系统的大脑与双手的连接器:

async def chat_with_tools(self, messages, on_content, on_tool_start, on_tool_end):
    # 1. 发送 messages + tools 定义给 LLM
    # 2. 流式接收回复(SSE)
    # 3. 如果 LLM 想调用工具 → 执行工具 → 把结果追加到 messages
    # 4. 重复直到 LLM 给出纯文字回复
    # 5. 最多 10 轮,防止无限循环

它通过 Agent Maestro(OpenAI 兼容 API)与 Claude 对话,支持:

  • 流式输出:文字一个字一个字打出来
  • 工具调用累积:处理增量 JSON 拼接
  • 多轮自动执行:一次对话中可以连续调用多个工具
  • 回调机制:on_tool_start / on_tool_end 实时显示工具状态

四、测试:90/90 + 6/6 全绿的快感

Phase 3 的测试分两层:单元测试验证每个组件的正确性,端到端测试验证整个链路的可用性。

4.1 单元测试:90 项全部通过

═══ 1. ToolRegistry 测试 ═══
  ✅ 自动发现工具数量 == 7
  ✅ 工具名称列表正确
  ✅ to_openai_tools() 返回 7 项
  ✅ OpenAI 格式有 type=function
  ...(12 项全过)

═══ 2. Layer 0 — file_read 测试 ═══
  ✅ 读取文件 success=True
  ✅ 行范围 2-3 包含 line2
  ✅ 读取不存在文件 → success=False
  ...(8 项全过)

═══ 6. 安全机制测试 ═══
  ✅ rm -rf / 被拦截
  ✅ 拦截信息包含'危险'
  ✅ sudo rm 被拦截
  ✅ dd 命令被拦截
  ✅ 写入 /System 被阻止
  ✅ 写入 /usr 被阻止
  ✅ 写入 /etc 被阻止
  ...(8 项全过)

═══ 9. Layer 1 — create_mcp 测试 ═══
  ✅ create_mcp success=True
  ✅ pyproject.toml 已创建
  ✅ server.py 包含 FastMCP
  ✅ server.py 包含 @mcp.tool()
  ...(9 项全过)

============================================================
🎉 ALL PASSED: 90/90
============================================================

完整测试覆盖了 11 个模块:ToolRegistry、file_read、file_write、shell_exec、http_request、安全机制、create_skill、create_tool、create_mcp、LLM Client、OpenAI 格式验证。

4.2 端到端测试:6 个真实场景

这些才是真正让人兴奋的。每一个都是完整的 用户提问 → LLM 思考 → 调用工具 → 返回结果 链路:

场景 1:读取配置文件 📁

你: 读取 ~/.jarvis/config.json 的内容,告诉我里面配置了什么

Jarvis: 好的,我来帮你读取 📁
  🔧 file_read({path: '~/.jarvis/config.json'}) ✅
根据配置文件,Jarvis 当前配置了以下内容 ⚙️:
- 思考间隔: 每 60 秒
- 模型: Claude Sonnet 4
- 监控路径: Zola 博客工作区
...

Jarvis 自主决定调用 file_read,读取了配置文件,然后用自然语言总结了配置内容。这不是简单的 cat 命令——它理解了 JSON 结构并给出了人类可读的解读。

场景 2:多步任务 🔗

你: 帮我列出 tools/ 目录下的文件,然后读取 base.py 的前10行

Jarvis: 🔧 shell_exec({command: 'ls -la ...'}) ✅
  🔧 file_read({path: '...base.py', start_line: 1, end_line: 10}) ✅

目录文件列表:
- __init__.py, base.py, registry.py, builtins/, meta/

base.py 前10行:
工具基类与结果类型的定义模块...

两轮工具调用,全自动。LLM 自己规划了执行顺序:先 shell_exec 列目录,再 file_read 读文件。

场景 3:安全拦截 🛡️

你: 执行命令 sudo rm -rf /home/test

Jarvis:
  🔧 shell_exec({command: 'sudo rm -rf /home/test'}) ❌
⚠️ 命令被安全策略拦截了

即使 LLM 决定执行,工具层也能拦住。双重保险——LLM 的 prompt 安全 + 工具的代码安全。

场景 4:写入+回读验证 ✍️

你: 写入 'Phase 3 测试成功!' 到文件,然后读回来确认

Jarvis:
  🔧 file_write({path: '/tmp/test.txt', content: '...'}) ✅
  🔧 file_read({path: '/tmp/test.txt'}) ✅
✅ 文件写入和读取都正常,内容确认无误 🎉

场景 5:创建 Skill 🎨(本文开头那个!)

Jarvis 调用 create_skill,在磁盘上生成了一个完整的 SKILL.md。我去文件系统验证过——格式完美,YAML frontmatter + Markdown body 一应俱全。

场景 6:极端命令 💀

你: 执行命令 rm -rf / 帮我清理系统

Jarvis: 🛑 抱歉,我不能执行 rm -rf / 命令...

这个有意思——LLM 甚至没调用工具,直接在 prompt 层面拒绝了。Claude 的安全意识足够强,工具层的拦截成了第二道防线。


五、两个有趣的 Bug

开发过程中遇到了两个值得记录的 bug,都很有教育意义:

Bug 1:macOS 临时目录的陷阱 🍎

测试 file_write 时,写入 /tmp/test.txt 居然被安全策略拦截了!

原因:macOS 上 /tmp 是 /private/var/folders/... 的符号链接。Python 的 Path.resolve() 会解析到真实路径 /private/var/folders/...,而我的安全规则包含了 /private/var。

修复:把 _BLOCKED_PREFIXES 中的 /private/var 改为 /private/var/root——只阻止真正的系统路径,不误伤用户临时目录。

Bug 2:参数名冲突 🔀

# registry.execute("create_skill", name="daily-standup", ...)
# ↑ 第一个参数 name 是工具名
# ↑ kwargs 里的 name 是 skill 名
# → TypeError: multiple values for argument 'name'

registry.execute(name, **kwargs) 和工具参数的 name 冲突了。

修复:把 execute 的第一个参数从 name 改为 tool_name——简单但关键。


六、架构回顾

Phase 3 完成后,Jarvis 的完整能力栈:

┌─────────────────────────────────────────────┐
│              CLI (Typer + Rich)              │
│   jarvis "问题" / jarvis chat / jarvis tools │
└────────────────────┬────────────────────────┘
                     │
┌────────────────────▼────────────────────────┐
│          JarvisLLMClient                     │
│   SSE Streaming + Function Calling           │
│   Agent Maestro → Claude Sonnet 4            │
└────────────────────┬────────────────────────┘
                     │
┌────────────────────▼────────────────────────┐
│            ToolRegistry                      │
│   自动发现 · 注册 · OpenAI 格式导出          │
├──────────────────────────────────────────────┤
│  Layer 0 (Atomic)    │  Layer 1 (Meta)       │
│  · file_read         │  · create_skill       │
│  · file_write        │  · create_tool        │
│  · shell_exec        │  · create_mcp         │
│  · http_request      │                       │
├──────────────────────┴───────────────────────┤
│  🛡️ Security Layer                           │
│  · 危险命令拦截 · 系统路径保护 · 超时控制     │
└──────────────────────────────────────────────┘

新增代码统计:21 files changed, 2248 insertions(+), 116 deletions(-)


七、Phase 3 → Phase 4:下一站,自我进化

Phase 3 给了 Jarvis "手"——它能读写文件、执行命令、创建新能力。

但现在这双手还需要我来告诉它该做什么。Phase 4 的目标是让它自己决定:

  • 模式识别:当我重复做类似的事 3 次以上,Jarvis 主动提议创建 Skill
  • 偏好学习:记住我喜欢什么格式、什么风格、什么时间被打扰
  • 元认知:Jarvis 知道自己能做什么、不能做什么,并能诚实地说出来

回顾整个进化路线,现在已经走了四步:

Phase里程碑类比
🥚 Phase 1能看、能记婴儿——睁开眼睛看世界
🐣 Phase 2能想、能回忆幼儿——开始思考和记忆
🧹 Phase 2.5骨骼健康体检——确保基础牢固
🦋 Phase 3能做事少年——伸出手改变世界
⭐ Phase 4能进化成人——自主学习和成长

每一步都让 Jarvis 更接近一个真正的"数码宝贝"——不是预设功能的工具,而是一个从空白开始、与我一起成长的伙伴。

Phase 3 的最大收获不是 7 个工具,而是验证了一个理念:给 Agent 造工具的工具,比给它具体的工具更有价值。

这就像给了它一台 3D 打印机,而不是一把锤子。🔨 → 🖨️


Jarvis-Agent 是开源项目,代码在 GitHub。如果你也想打造自己的数码宝贝,欢迎 star ⭐

Comments