文章

草船借箭:在你的微服务里嵌入 Claude Code

草船借箭:在你的微服务里嵌入 Claude Code

1. 背景

最近我在设计一个代码仓库智能分析系统,用来承接代码分析、诊断等类似的开放性任务请求,最典型的场景是辅助排障。

它要做的事情大概是:业务方过来一个代码仓库或应用名、一个问题、一些上下文,然后系统自己去拉代码、读代码、跑 grep、整理调用链,最后返回一个结构化结论。

如果只看这个描述,很容易把问题理解成“调一下 LLM API”。但真到工程实现阶段,会发现这事没那么简单。

代码仓库分析和洞察生成,本身就是一个非常复杂的任务。它不是把一段文本塞给模型,然后等一个答案回来。

它要在大量文件、模块边界、调用链、历史上下文之间来回穿梭,最后从一堆局部证据里提炼出一个可靠结论。

所以它更接近 ReAct 范式:先观察问题,再决定要不要读文件、搜符号、跑命令;拿到工具结果后继续推理,必要时再发起下一轮 action。

问题也就自然转到了工程实现上。

2. 直观思路

最直观的方案当然是自己实现一套 agent runtime。

也就是直接调用模型 API,然后在服务端自己维护前面提到的那套 ReAct loop。

这条路没有问题,而且从架构控制力上看还挺干净。所有协议、工具、权限、状态都在自己手里,理论上扩展自由度很高。

但工程成本也很真实。因为一旦进入代码分析场景,问题马上就会变成:

  • 怎么让模型稳定选择 ReadGrepBash 这些工具?
  • 工具调用结果怎么截断、压缩、回灌?
  • 多轮上下文怎么保存和恢复?
  • Bash 这种危险能力怎么审批?
  • 中途用户拒绝某个工具调用时,模型怎么继续?
  • MCP server 怎么接进来?

这些问题都能做,但不是“调一下 LLM API”能解决的。真自己做,差不多就是重新写一个小号 Claude Code。

顺着这个思路再往前想一步:我们天天都在用 Claude Code 分析代码、修改代码,那能不能把它嵌进业务微服务里,直接复用它已经做好的 agent runtime?

这就是标题里说的【草船借箭】:不是自己造箭,而是先把 Claude Code 这套成熟能力借过来。

3. fork CLI 进程

最朴素的做法

Claude Code 最常见的入口是 CLI。既然它是一个命令行工具,最朴素的集成方式就是:

  1. 服务端启动一个 claude 子进程。
  2. 往 stdin 写入用户问题。
  3. 从 stdout 读取 Claude Code 的输出。
  4. 把输出转成业务系统需要的结果。

这个思路就像在服务端调用 git clone 拉代码、调用 grep 搜关键字、调用 go test 跑测试一样。我们平时集成外部工具,经常就是这么干的,朴素但管用。

Claude Code 不一样

但 Claude Code 和普通命令行工具不太一样。

普通工具往往是【输入参数 -> 执行 -> 输出结果】。

Claude Code 则是一个持续运行的 agent runtime。它中间会流式输出,会调用工具,会等待权限确认,也可能继续追问下一轮输入。

如果直接 fork 一个默认【交互模式】的 CLI 进程,然后模拟终端输入输出,很快会遇到几个问题:

  • 终端富文本不好解析:普通 CLI 面向人,会有颜色、进度、布局、交互提示。
  • 事件边界不好判断:哪一段是 assistant 文本,哪一段是工具调用,哪一段是最终结果,不适合靠字符串硬拆。
  • 权限请求不好回写:Claude Code 要执行工具时,宿主进程需要知道它在请求什么,并返回 allow / deny。
  • 错误和诊断不好收口:stdout、stderr、最终结果混在一起,服务端很难做稳定协议。

所以,fork CLI 进程这个方向没错,但不能停留在默认的【交互模式】。人能看懂的东西,程序未必好处理。

我们真正需要的,是通过参数把 Claude Code 切到【JSON 模式】,让它直接输出程序好处理的 JSON。

4. JSON 模式

核心参数

好在 Claude Code 本身就考虑到了这类场景,核心参数如下:

1
2
3
4
5
claude \
  -p \
  --output-format stream-json \
  --input-format stream-json \
  --verbose

如果想看当前版本的完整参数解释,直接跑 claude -h 就行。

这里最关键的是三个参数:

  1. -p / --print:进入非交互的 print 模式,不走普通终端 REPL。
  2. --output-format stream-json:stdout 输出实时 JSON 事件流。
  3. --input-format stream-json:stdin 输入也走 JSON 消息流。

stream-json 是什么

stream-json 可以理解成 NDJSON:一行一个 JSON 对象。对程序员来说,这个信号就很明确了:宿主进程不用从终端富文本里抠信息,而是可以按 JSON 字段和事件类型来处理。

通俗来说,这里就是从【交互模式】切到【JSON 模式】:

1
2
3
4
5
交互模式:
  终端输入 -> 终端富文本输出

JSON 模式:
  JSON 输入 -> JSON 事件输出

这才是嵌入式 Claude Code 的关键。不是去模拟一个人坐在终端前敲命令,而是让宿主进程通过稳定的 JSON 事件和 Claude Code 通信。

很多时候我们说”把 Claude Code 嵌入微服务”,听起来像是把一个 App 塞进服务端。其实不是。更准确地说,是把 Claude Code CLI 当成一个提供结构化 I/O 的受控子进程来管理。

当然了,CLI 参数和事件字段都属于工具协议,后续可能随着版本调整。这里讲的是思路,真正落地时还是要以当前版本文档为准。

5. Agent SDK

再往上一层,就是官方 Claude Agent SDK

前面说的 JSON 模式已经足够让程序接入 Claude Code,但如果每个业务系统都自己处理 stdin、stdout、NDJSON、权限事件,那还是会写不少胶水代码。

Agent SDK 的价值就在这里。它不是让我们再实现一套 Claude Code runtime,而是把 Claude Code 的能力包装成更适合应用代码调用的 SDK。

底层还是 agent runtime,上层拿到的是更顺手的对象、迭代器和 callback。

flowchart LR
    Service[业务微服务]
    SDK[Agent SDK]
    Runtime[Claude Code CLI]
    Service --> SDK
    SDK --> Runtime
    Runtime --> SDK
    SDK --> Service

换句话说,直接接 CLI JSON 模式,适合理解原理,也适合做最小验证。

用 Agent SDK,则是把这些协议细节再往上收一层,让业务代码更像是在调用一个库,而不是在管理一段 stdin / stdout 协议。

6. Go 怎么办

问题来了:官方文档目前主要提供 TypeScript / Python SDK,Go 服务怎么办?

这里大概有两个选择。

用社区 binding

第一种是先用社区 Go binding。比如 severity1/claude-agent-sdk-go 这类库,本质上也是在 Go 里补一层 SDK 胶水代码,帮你管理 Claude Code 子进程和消息流。

如果只是想先验证思路,直接用现成 binding 把链路跑通,ROI 往往更高。

社区 binding 适合快速验证,但不能直接等同于官方兼容性承诺。生产前要确认它能跟上 CLI 协议变化,以及子进程退出、超时、stderr 保留、权限回调这些边界是否符合服务端要求。

自己写胶水代码

第二种是自己实现一层很薄的 Go 胶水代码。

注意,这里不是重新实现 Claude Code,也不是重新实现 ReAct runtime。我们只是实现:

1
2
3
4
5
6
Go 微服务
  -> 启动 Claude Code CLI 子进程
  -> 写入 stream-json
  -> 读取 stream-json
  -> 处理权限请求
  -> 转成自己的业务结果

有个很实用的参考:官方的 TypeScript / Python SDK 都是开源的,可以直接读源码,看它是怎么管子进程、解析事件流、处理权限回调的。照着官方实现翻译成 Go,比自己从头摸索协议要快很多。

7. 嵌入后的边界

把 Claude Code 嵌入业务微服务后,最容易出问题的不是“能不能跑起来”,而是边界治理。

这里有几个点要提前想清楚。

  1. 工作区隔离。 每个任务都要有独立 workspace。代码 clone、临时文件、Claude Code session、调试产物都放到任务目录下。不要让两个任务共享工作目录,否则 agent 读历史、改文件、恢复 session 时都可能互相污染。
  2. 运行环境隔离。 如果要开放 Bash 之类的能力,不要直接跑在业务微服务所在的宿主环境里。更稳妥的方式是把每个任务放进容器或沙箱,限制文件系统、网络、环境变量和执行时长。这样即使 agent 跑偏了,影响面也被关在任务环境里。
  3. 权限控制。 服务端不能照搬本地 CLI 的交互方式。默认可以允许 ReadGrepGlob,谨慎开放 Bash,写文件类工具只允许写 task workspace。危险操作必须回到服务端审批。
  4. 成本和可观测性。 本地使用 Claude Code,跑久一点顶多是自己肉疼;服务端一旦开放给业务系统,就要关心任务时长、token 消耗、子进程回收、stderr 诊断日志、最终结果归档。
  5. 输出收口。 业务系统最终需要的不是一段聊天记录,而是可以继续流转的结构化结果。比如问题结论、证据列表、风险等级、建议动作,这些要在系统边界上约束清楚。

这些东西和 Claude Code 本身聪不聪明关系不大,但决定它能不能作为服务端能力长期运行。否则 demo 能跑,不代表上线之后也能稳。

8. 总结

一路走下来,可以把这件事总结成几句话:

  1. 代码分析不是单轮模型调用,而是 ReAct 式的 agent runtime。
  2. 自研 runtime 成本很高,复用 Claude Code 是一条很务实的路。
  3. 普通 fork CLI 进程不够稳定,关键在于使用 Claude Code CLI 的 stream-json【JSON 模式】。
  4. Agent SDK 可以理解成一层应用友好的封装,让业务代码少处理底层 I/O 和权限细节。
  5. Go 没有官方 SDK 也不是死路,可以先用社区 binding,也可以自己实现一层薄胶水代码。

总而言之,嵌入 Claude Code 的核心不是”调模型”,而是【管理一个可交互的 agent runtime】。

这篇先把思路和边界讲清楚。至于具体实现,反而不用一开始就写得太死。方向定好、边界划清楚,剩下就是把问题拆小,让 AI coding 一段一段实施。

以上是我目前对这套方案的理解,算是抛砖引玉。如有理解不准确的地方,欢迎指正。

9. 参考

本文由作者按照 CC BY 4.0 进行授权