TL;DR — 我们逆向了 Claude Code 的 TypeScript 源码,搞清楚了它的 Agent 架构如何处理安全、复杂任务和工具权限。然后把这些模式用到了一个开源项目上——让玩家在泰拉瑞亚游戏里跟 AI 聊天,AI 还能给道具、改天气、传送玩家。以下是我们的发现、实现过程和踩坑总结。


为什么要拆 Claude Code 的源码

Claude Code 不只是个编程助手。底层它是一个 Agent 运行时——会 spawn 子 Agent、管理文件权限、跑 bash 命令、判断什么时候该问用户什么时候该直接做。我们想搞清楚它的内部机制,然后把这些想法用到一个完全不同的场景:泰拉瑞亚游戏服务器。

我们的项目 terra_llm_bridge 把泰拉瑞亚 TShock 服务器接到了一个 LLM 上。玩家在聊天框打 @ai 就能跟 AI 对话——但 AI 不止能聊天,还能做事:给道具、改天气、传送玩家,甚至能切换困难模式。最后那条就是我们翻车的地方。

第一次有玩家让 AI 设成雨天,LLM 自作主张调了 terra_world_hardmode(confirm=True)——把整个服务器的世界不可逆地切成了困难模式。没人要求它这么做。模型自己觉得该做就做了。

我们需要一个真正的权限系统。于是去翻 Claude Code 的源码。


Claude Code 的 7 层权限架构

通读 src/utils/permissions/permissions.ts 的约 1500 行代码,加上 Agent 工具的基础设施(约 3800 行),一套清晰的架构浮现出来。Claude Code 不是靠单点检查做安全——它有七层

Layer 1a: 拒绝规则   →  "永远不允许 Bash(git push --force)"
Layer 1b: 询问规则   →  "Bash(curl *) 总是弹窗确认"
Layer 1c: 工具自检   →  每个工具 checkPermissions() 自己的逻辑
Layer 1d: 工具自拒   →  Read 工具白名单特定路径
Layer 1f: 内容规则   →  "就算 bypass 模式,npm publish 也要弹窗"
Layer 1g: 安全检查   →  ".git/、.claude/ 永远不能绕过用户确认"
Layer 2:  模式旁路   →  bypassPermissions / auto / acceptEdits / dontAsk
Layer 3:  YOLO 分类器 →  AI 读全文 transcript,判断是否安全

最有意思的是 YOLO 分类器——一个独立的小模型,读取完整对话记录,把每次工具调用分类为安全或危险。两阶段系统:快速分类器处理明显 case,深度思考分类器处理边界情况。

但对我们最有用的不是 AI 分类器。而是 Claude Code 如何在结构上防止某些工具在错误的上下文中被调用——通过工具白名单、黑名单和子 Agent 特化。


Agent 模式:不是多 Agent 协作,而是专项 Worker

Claude Code 用的不是"Agent 之间协商谈判"的多 Agent 协作。它是一个主协调器 + 专项 Worker

主 Agent(Tool Calling,全部工具)
  │
  ├─ 简单: "读文件 X" → Read 工具
  │
  └─ 复杂: "审计这个分支" → Agent("Explore")
                              │
                              ├─ 工具: [Read, Grep, Glob]  ← 白名单
                              ├─ 禁止: [Edit, Write]        ← 黑名单
                              ├─ 系统提示: "你是文件搜索专家"
                              └─ 返回结果 → 主 Agent 行动

每个子 Agent 类型由三要素定义:

  1. 工具权限(白名单 + 黑名单)——能碰什么
  2. 系统提示——角色专属指令
  3. 模型——Explore Agent 用 Haiku(便宜),Plan Agent 用 Sonnet(推理强)

核心洞察:主 Agent 不会变更复杂。它保持简单,只有一个 Agent 工具让它把复杂任务委派出去。子 Agent 就是另一个 Tool Calling 循环,只是工具受限 + 提示词不同。

这套架构的可组合性是关键:每个零件简单,但组合起来能处理单个 prompt 消化不了的复杂度。


我们怎么把这个模式用到 terra_llm_bridge

我们的泰拉瑞亚桥接比 Claude Code 简单——46 个工具而非几百个,“安全问题"是"别在玩家问天气时切 hardmode"而不是"别让 AI rm -rf /"。但模式是直接可以搬的。

问题

改之前:LLM 同时看到所有 46 个工具。当玩家问"给我最强套装”,LLM 会并行wiki_search 查资料 + give_item 给东西——一边查 wiki 一边已经预判了 Solar Flare 套装。有时候猜对,有时候给召唤师玩家塞了一套战士装备。

解决方案:两阶段工具开放

我们没有加子 Agent——46 个工具不需要。但我们在 graph 层面用了工具限制模式

route → llm(研究)  ⇄  tool      →  escalate  →  llm(行动)  ⇄  authorize  ⇄  tool  →  output
        17 个只读工具                          46 全工具      关键词 gate
        wiki、lookup、状态                       give、kick、spawn

图有两个阶段:

研究阶段——LLM 只拿到 17 个只读工具(wiki_search、item_lookup、player_list、world_info 等)。它不能调 give_item、kick、spawn 或任何破坏性工具。先查资料。

升级(escalate)——当 LLM 输出文本(没有更多 tool_call),图自动切到行动模式,注入提示:“你现在可以访问全部工具了。”

行动阶段——LLM 拿到全部 46 个工具,可以对研究发现做出行动。

这是结构层面强制执行的,不是 prompt 建议。LLM 在研究阶段根本调不了 give_item,因为这个工具没绑定。

权限 Gate

在两阶段拆分之前,我们还加了 authorize_node——LLM 和 ToolNode 之间的硬拦截,检查玩家聊天最近的消息是否包含该工具领域的关键词:

1
2
3
4
5
6
GATED_TOOLS = {
    "terra_world_hardmode": {"hardmode", "hard mode", "肉山", "困难模式"},
    "terra_player_kick":    {"kick", "踢出", "踢了"},
    "terra_server_stop":    {"stop server", "关服", "停服"},
    # ... 还有 8 个
}

如果玩家说"设个雨天试试"而 LLM 想调 world_hardmode,authorize_node 检查:玩家最近的消息里有 hardmode 相关的关键词吗?没有?拦截。 这个工具调用被替换成 BLOCKED 消息,ToolNode 根本看不到。

这是粗过滤器——它检查的是玩家提到了什么,而不是玩家请求了什么。“上次打肉山的时候"会通过关键词检查,尽管玩家没要求开 hardmode。但粗够了:目标是拦截灾难性的跨界调用(天气 → hardmode),不是完美理解意图。


我们选择不做的

没有 YOLO 分类器

Claude Code 的 AI 分类器读完整 transcript,用另一个模型判断工具调用是否安全。我们没做,因为:

  • 增加延迟——每次 gated 工具调用前多一次 LLM 请求
  • 泰拉瑞亚聊天风险低——给错套装可以补救
  • 关键词匹配已经能拦住灾难性 case

没有子 Agent 派生

Claude Code 为复杂任务 spawn 子进程。我们不需要:

  • 泰拉瑞亚工具面小(46 个)
  • 多轮工具调用已经能处理我们实际面对的场景
  • 给游戏聊天机器人 spawn 子进程是过度工程

没有 ReAct 模式

经典的 Thought → Action → Observation 循环会增加 token 消耗,但不改变我们的核心能力。DeepSeek 的 thinking tokens 已经承担了推理,而两阶段工具访问比基于 prompt 的 ReAct 更可靠地强制了"先研究再行动”。


一张图看清架构

┌──────────────────────────────────────────────────────────┐
│  泰拉瑞亚服务器(TShock + C# 插件,24 个游戏 Hook)        │
│  玩家输入 "@ai 给我最好的套装"                             │
└──────────────────────┬───────────────────────────────────┘
                       │ JSON webhook
┌──────────────────────▼───────────────────────────────────┐
│  Python aiohttp 监听器 (:9876)                            │
└──────────────────────┬───────────────────────────────────┘
                       │
┌──────────────────────▼───────────────────────────────────┐
│  LangGraph StateGraph                                     │
│                                                           │
│  route  →  llm(研究)  ⇄  tool      17 只读工具           │
│               │                                           │
│          escalate  →  llm(行动)  ⇄  authorize  ⇄  tool   │
│                          46 全工具      关键词拦截         │
│               │                                           │
│             output  →  广播到游戏聊天                      │
│                                                           │
│  记忆: AsyncSqliteSaver 按玩家(thread_id)持久化          │
└──────────────────────────────────────────────────────────┘
                       │
         ┌─────────────┴──────────────┐
         ▼                            ▼
   TShock REST API              Terraria Wiki API
   (give / kick / spawn)        (terraria.wiki.gg)

源码分析的启示

读 Claude Code 源码教会我们三件事,适用于任何 Agent 项目:

1. 安全是分层的,不是二元的。 一个 confirm 参数对 LLM 来说只是软建议。真正的安全需要结构性约束——LLM 不该能调用它无权使用的工具,就像 Web 服务器不该让你访问没有权限的端点,不管你怎么礼貌地请求。

2. 工具限制是最便宜也最可靠的安全形式。 Claude Code 的 Explore Agent 之所以"只读",不是因为 prompt 写了——是因为 Edit 和 Write 不在它的工具列表里。我们的研究阶段之所以"先查资料",不是 prompt 建议——是因为 give_item 根本没绑定。你不能通过 prompt injection 绕过不存在的工具。

3. 特化胜过复杂化。 Claude Code 的子 Agent 不比主 Agent 更聪明——只是更受约束。更少的工具 + 聚焦的 prompt = 更可靠的行为。我们的两阶段系统同理:先限制,准备就绪再扩展。


关于这个项目

terra_llm_bridge 是一个连接泰拉瑞亚游戏服务器与 LLM 的开源项目。功能包括:

  • 24 个游戏 Hook——自研 C# TShock 插件捕获聊天、Boss 击杀、死亡、登录等 24 种事件
  • 46 个管理工具——给道具、管玩家、控天气、召 NPC、管区域和权限
  • 两阶段 Agent——研究(17 工具)→ 行动(46 工具)
  • 硬权限 Gate——基于关键词的 authorize_node 拦截未授权工具调用
  • MCP 服务端——同 46 工具暴露给 Claude Code 做服务器管理
  • 持久化记忆——通过 LangGraph AsyncSqliteSaver 按玩家保持对话历史

项目目前处于活跃测试阶段,尚未发布到 GitHub。我们在私有泰拉瑞亚服务器上运行,迭代 Agent 架构后再开源。如果对代码感兴趣或想提前体验,欢迎联系。


技术栈:Python 3.14, LangGraph 1.x, DeepSeek(Anthropic 兼容 API), C# .NET 9, TShock v6.1.0, aiohttp, httpx.