Skip to main content

做一个兼容 Claude Code 的内部 Agent 平台

做一个兼容 Claude Code 的内部 Agent 平台

背景

这半年主要在做一个内部项目,代号 CC-Agent。起因是公司内部有这么几个绕不开的需求,现成的 Claude Code 直接用满足不了:

  • 很多场景在内网,用的是国产模型或自建网关,官方默认的鉴权和上行链路走不通。
  • 用 Agent 的不只有写代码的人,还有需求分析、运维、设计这些岗位,他们不会开终端。
  • 希望把各岗位的领域知识和工作规矩沉淀进去,让 Agent 在不同岗位上各管各的事,而不是一个什么都能聊但谁都管不住的助手。
  • 已经有很多沉淀是使用claude code项目初始化实现的,这些沉淀不能丢弃

所以把内核重写了一遍,自己掌控每一层。下面按从底到上的顺序记一下这几层分别做了什么。

兼容 Claude Code,兼容的是什么

“兼容 Claude Code”不是接一下它的 API 就行。Claude Code 本身是个跑在本地的程序,要兼容的是它的行为约定,主要三块。

Agent 循环。 Agent 本质是个循环:模型说要调工具,程序真的去执行,把结果塞回去,模型接着往下想。难点都在细节里——工具调用要并发、长输出要截断、上下文满了要压缩、会话断了要能原地接上。这部分在 src/ 下重写,核心是 QueryEngineTask,外加一个专门跟踪会话生命周期的诊断服务。 工具集。 读写文件、改文件、跑命令、搜索这些工具的语义必须跟 Claude Code 对齐,否则同一段提示词在两边跑出来的结果就对不上。工具实现单独放在 src/tools,一个工具一个文件。

配置和状态的兼容。 这块最容易被忽略也最坑。Claude Code 把会话记录、provider 配置、技能、MCP 配置、OAuth token 都写在 ~/.claude/ 下。定的规矩是:这些文件属于用户,不属于我们。读写时未知字段原样保留,只能增量合并不能整段覆盖,更不能往里塞自己的版本号。任何动持久化格式的改动都要配一个”老数据也能读”的回归测试。这条写进了项目约定,AI 改代码时也得守。

兼容的目标是:一段原本给 Claude Code 写的提示词、技能、配置,搬过来还能照常跑。

本地服务和多 Provider 路由

内核之上是一个本地服务,Bun 写的,跑在 3456/3457 端口,做两件事:

  • 对外提供 HTTP 接口和 WebSocket,给桌面端、IM 用。
  • 对内做 Provider 路由。配几个模型来源(官方、国产、自建网关都行),由它决定走哪个、怎么转发、限流了怎么提示、超时了怎么重试。

这一层花时间最多的不是功能,是质量门禁。Agent 行为一旦跑偏,表面上看不出来,得靠成体系的测试兜底。约定里有一条”功能质量契约”:

  • 改了哪块代码,同一个 PR 里就得带上那块的测试。
  • 涉及 Agent 循环、工具执行、Provider 路由、会话恢复这些核心路径的,光有 mock 测试不够,还要在有真实模型的机器上跑一遍 live 基线。
  • 改动到的每一行可执行代码都要过”变更行覆盖率”这道闸。

平时一条 bun run verify 把整套门禁跑完,报告落在 artifacts/ 下。这样 AI 改完代码后,不是它说”完成了”就算完成,而是门禁通过了才算。

接进 IM

再往前一步,是让 Agent 出现在大家本来天天用的聊天软件里。于是有了 IM Adapter,目前接了企业微信、钉钉、飞书、Telegram 四个。

链路是这样:

聊天软件(企微 / 钉钉 / 飞书)
  -> adapters/<平台>/index.ts
  -> 本地服务 /api/sessions + /ws/:sessionId
  -> Claude Code 会话

各平台的差异(消息格式、鉴权、回调)收在各自目录里,公共的部分——消息缓冲、去重、会话存储、配对、权限——抽到 adapters/common 复用。踩的坑基本都是 IM 特有的:消息会重复推送要去重、长回复要分段、用户得先配对才能用(不然谁都能调)。

配置统一放在桌面端的”设置 → IM 接入”里,配完写到 ~/.claude/adapters.json,Adapter 进程读这个文件再起来。这里有意没做自动拉起进程——IM 机器人什么时候上线得人说了算,不能服务一启动就把机器人也偷偷连上去。

用 CLAUDE.md 约束出来的领域知识库

前面四层是平台,这一层才是真正让它好用的地方——领域工作台。

通用 Agent 有个老问题:什么都能聊,但什么都不专业,还容易被带跑偏。想让它专心做需求分析,它能扯到写代码去。我们的做法是给每个岗位单独开一个工作台,每个工作台配一份 CLAUDE.md 当”宪法”,把这块业务的规矩、命令、边界都写死在里面。

现在有几个工作台:需求分析、机电、软件、设计,还有一个我想重点讲的设备软件知识库。

不走 TopK 向量检索的 LLM 知识库

给 LLM 配知识库,主流是 RAG:文档切片、做 embedding 存进向量库,提问时按相似度取 TopK 片段拼进上下文。这套在通用问答上够用,放到工程知识库场景里有几个绕不开的问题。

  • 取回的是片段不是整篇。一个模块的设计被切成几十块,TopK 命中其中三五块,模块之间的因果和上下文就断了。

  • 效果吃 embedding 模型和切片策略。同一个问题,换模型、换 chunk 大小,召回可能完全不同,调起来偏玄学。

  • 召回只看”像不像”,不看”对不对”。命中一段相关但过时的内容,模型照样当真往下答。

  • 多一层基础设施。文档一改就得重新 embedding,还要管同步和版本。 这个工作台没用向量库,走的是另一条路:结构化文档加全文检索加显式双链索引,让 Claude Code 直接在文件层面工作。对应的好处是:

  • 命中的是整篇结构化文档,模块的设计、接口、上下文完整,不会被切碎。

  • 检索靠全文检索加 frontmatter 结构化过滤,结果可复现、可解释,不绑定某个 embedding 模型。

  • 文档由源码反推、人可校对,没代码依据处明确标注,从源头压住编造。

  • 没有向量库这层,改完文档跑一次索引即可,维护成本低。

具体场景是给一类工业设备的上位机软件做工程知识库。文档不是人手写的,是拿源码反推出来的——把设备软件源码按模板反推出各模块的详细设计、界面使用说明和系统架构。具体的设备型号、工艺名称这里不写。

整个库分三层,职责分得很清楚:

装什么谁维护
raw/原始文档(设计 / 模块 / 界面说明)人编辑,AI 只能增改、不能删
wiki/自动索引、操作日志、分类索引AI 维护
.sync/与外部 wiki 系统的同步元数据AI 维护,不进版本库

用起来就是几个斜杠命令:

/new modules <模块>        # 从模板新建文档
/query <自然语言问>         # 全文检索并回答
/index modules               # 重建索引
/lint                        # 全库体检:断链、缺字段、孤儿页

值得说的是背后那套硬规矩,全写在 CLAUDE.md 里:

  • 每篇文档必须有 frontmatter(类型、标题、分类、创建时间、标签),文件所在目录要和声明的分类一致,对不上不让过。
  • 双链必须从库根写完整路径,不能图省事写半截,否则 Obsidian 的关系图谱连不起来。
  • 任何操作都要追加日志,日志文件只能往后加、不许改历史。AI 拿不准的、有冲突的、外部系统没配好的,都写进一个待审队列,不许默默跳过。
  • 文档是源码反推的,没代码依据的地方要标注”代码中未体现”或”推断”,不许编。知识库最怕的不是缺内容,是混进看着像真的假内容。

还有一个我比较在意的设计:工作台边界。这个知识库只接知识库管理的活,让它去写源码、做需求分析、闲聊,它会用一段固定话术礼貌地把你引到对应的工作台,就算用”帮我顺便看一下”想绕过去也不破例。

实现上是服务端在每次会话启动时,通过 --append-system-prompt 追加一段不可见的边界约束,和 CLAUDE.md 里写给人看的那段对齐。约束写两份,一份给人读,一份真正管住模型。

一点记录

做下来体会比较深的一点:做 Agent 平台,难的不是接模型,是定规矩。模型能力是外部给的,会一直变强;但某个 Agent 在某个岗位上该做什么、不该做什么、动了哪些文件、有没有编造,这些边界得自己一条条立。

这些规矩落在三个地方:代码里的质量门禁保证平台本身不退化,持久化兼容契约保证不把用户的数据搞坏,每个工作台的 CLAUDE.md 保证 Agent 在自己岗位上守规矩。模型越强,这些围栏反而越重要。

后面把RAG以及初始Agent的坑也写一下。