普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月6日首页

01-想做 Code Agent,但不想只会调 API?我把 Claude Code 源码拆成了一套教程

作者 兔子零1024
2026年4月6日 16:18

关键词:Code Agent / Claude Code / CLI / Bootstrap / QueryEngine / Agent 架构 / 工程设计

别把 Code Agent 当聊天机器人:先看懂 Claude Code 的总架构和启动链路

很多人分析 Code Agent,一上来就盯着模型调用,结果越看越碎。

真正的问题不是“它调了哪个模型”,而是这套系统怎么从一个 CLI 命令,变成一个能长期执行任务的 Agent。Claude Code 的前两章其实就在回答这件事:一个 Code Agent 的骨架到底长什么样,它又是怎么被启动起来的。

一、先把范式分清:Chatbot、Copilot、Agent 不是同一种东西

从工程上看,这三者的差异不在 UI,而在执行边界。

类型 能做什么 不能做什么
Chatbot 一问一答、生成文本 不主动行动
Copilot 读编辑器上下文、给建议 通常不闭环执行
Code Agent 调工具、看结果、继续推进 不能没有状态机

Code Agent 的本质不是“更强的聊天”,而是下面这条循环:

flowchart TD
    U["用户输入"] --> C["组装上下文"]
    C --> M["调用模型"]
    M --> R{"返回类型"}
    R --> |"最终回答"| END["结束"]
    R --> |"tool_use"| T["调用工具"]
    T --> TR["工具结果回写历史"]
    TR --> M

只要系统进入这个闭环,它就不再是 Chatbot,而是执行器。

二、Claude Code 的总架构,其实是七块东西咬在一起

把源码抽掉细节,Claude Code 的主骨架是这样的:

CLI / Bootstrap
    ↓
QueryEngine / queryLoop
    ├─ Context Management
    ├─ Tool System
    ├─ Permissions / Hooks
    ├─ Skills / Plugins / MCP
    └─ UI Layer (Ink/React)

这里真正的主干不是 UI,也不是模型 SDK,而是中间三层:

  • queryLoop:负责让任务一轮轮继续;
  • Context:负责让模型每一轮都知道自己处在什么状态;
  • Tool System:负责把模型意图变成真实操作。

换句话说,Claude Code 不是“终端里包了一个 LLM”,而是“用 LLM 驱动的一套工具执行框架”。

三、CLI 启动的第一原则:快路径不能被慢路径拖累

Claude Code 在 cli.tsx 里先做了快速路径分流。像 --version 这种命令,根本不值得把主系统拉起来:

async function main(): Promise<void> {
  const args = process.argv.slice(2);

  if (args.length === 1 && (args[0] === '--version' || args[0] === '-v')) {
    console.log(`${MACRO.VERSION} (Claude Code)`);
    return;
  }
}

这个做法看起来普通,但它代表一个很成熟的 CLI 判断:

非主路径必须延迟加载,不能污染主路径的冷启动时间。

所以内部 MCP 模式、daemon worker、后台会话管理等路径,全部走动态 import()
不需要的模块,不在普通交互场景里承担启动成本。

四、main.tsx 做得最好的地方,不是功能多,而是“等待重叠”

进入 main.tsx 后,Claude Code 立刻做三件事:

import { profileCheckpoint } from './utils/startupProfiler.js';
profileCheckpoint('main_tsx_entry');

import { startMdmRawRead } from './utils/settings/mdm/rawRead.js';
startMdmRawRead();

import { startKeychainPrefetch } from './utils/secureStorage/keychainPrefetch.js';
startKeychainPrefetch();

这意味着:

  • 入口开始时先打性能点;
  • 企业配置读取立即启动;
  • Keychain 中的 token / API key 预取立即启动;
  • 后续模块加载继续往下跑。
sequenceDiagram
    participant M as main.tsx
    participant K as Keychain
    participant L as 模块加载

    M->>K: startKeychainPrefetch()
    M->>L: 继续加载 Ink / Commander / React
    par 并行
      K-->>K: 读取凭证
      L-->>L: 模块求值
    end

它不是在做复杂优化,而是在贯彻一个很基本的工程原则:把等待和计算重叠起来
CLI 工具的启动体验,往往就输赢在这种细节上。

五、执行模式必须尽早判断:交互式和无头模式根本不是一回事

Claude Code 很早就判断当前是不是非交互模式:

const hasPrintFlag = cliArgs.includes('-p') || cliArgs.includes('--print');
const isNonInteractive =
  hasPrintFlag || hasInitOnlyFlag || hasSdkUrl || !process.stdout.isTTY;

关键点不是 -p,而是这一句:

!process.stdout.isTTY

这意味着只要输出不是终端,比如:

  • 被管道消费;
  • 被重定向到文件;
  • 跑在 CI 里;

它就自动转成非交互模式。

这才是一个能被脚本和流水线真正利用的 Agent CLI。
如果一个 Agent 只能服务“人在终端前手动敲命令”的场景,它的工程价值会被大幅限制。

六、参数解析不是装饰,它定义了系统有多少种工作姿态

main.tsx 里用 Commander.js 注册了大量参数。重要的不是“参数多”,而是参数直接映射系统姿态:

  • --print:无头执行;
  • --bare:跳过 hooks、LSP、插件同步等附加能力;
  • --permission-mode:切换权限模型;
  • --model / --effort:改变推理策略;
  • --allowed-tools / --disallowed-tools:收紧或放宽工具池;
  • --add-dir:调整文件可访问范围。

这说明 Claude Code 不是只有一种运行方式。
它是同一套核心循环,在不同环境里切换不同外壳。

七、真正的入口不是 main(),而是 query()

CLI 解决的是“怎么启动”,真正决定 Agent 行为的是 src/query.ts 里的 query()

export async function* query(
  params: QueryParams,
): AsyncGenerator<StreamEvent | RequestStartEvent | Message | ...> {
  const consumedCommandUuids: string[] = []
  const terminal = yield* queryLoop(params, consumedCommandUuids)
  for (const uuid of consumedCommandUuids) {
    notifyCommandLifecycle(uuid, 'completed')
  }
  return terminal
}

最值得注意的是它为什么是 async function*

因为 Agent 不是“跑完再返回”的程序,而是会在过程中持续产生事件:

  • 模型输出;
  • 工具调用;
  • 工具执行进度;
  • 工具结果;
  • 结束原因。

如果不用 async generator,就很难同时做好实时 UI 和中间状态分发。

八、queryLoop() 是整套系统的心跳

真正执行循环的是 queryLoop()。它维护一份跨轮次状态:

type State = {
  messages: Message[]
  toolUseContext: ToolUseContext
  autoCompactTracking: AutoCompactTrackingState | undefined
  maxOutputTokensRecoveryCount: number
  turnCount: number
  transition: Continue | undefined
}

这份状态告诉我们,Claude Code 从来不是“多调几次模型”。
它是一个明确的状态推进器。

每轮循环都做这几件事:

  1. 取当前消息;
  2. 检查是否需要压缩;
  3. 调模型;
  4. 看到 tool_use 就派发工具;
  5. 收集结果回写历史;
  6. 判断是否继续。
flowchart TD
    A["准备消息"] --> B["必要时压缩上下文"]
    B --> C["调用模型"]
    C --> D{"出现 tool_use?"}
    D --> |"是"| E["执行工具"]
    E --> F["工具结果回写 messages"]
    F --> A
    D --> |"否"| G{"是否完成?"}
    G --> |"是"| H["返回 Terminal"]
    G --> |"否"| A

只要你理解了这一步,就会知道为什么很多 Agent Demo 看起来会动,但一进真实任务就塌:
它们没有把“任务推进”做成状态机,只是做成了多轮问答。

九、第一篇该记住的,不是某个函数,而是三条原则

Claude Code 的前两章合起来,核心其实只有三条:

1. Agent 不是聊天产品,而是执行系统

只要它进入“调用工具-观察结果-继续决策”的闭环,它就不是普通聊天。

2. 启动路径本身就是工程能力的一部分

快路径分流、并行预热、模式早判,这些都不是边角优化,而是主能力。

3. 一切最终都收敛到 queryLoop 这个状态推进器

CLI、参数、模式、上下文、工具,最后都只是为了让这条循环稳定工作。

最后

如果把 Claude Code 当成“会写代码的聊天机器人”,后面很多设计你都会看不懂。
只有把它当成一个长期运行的执行框架,你才会理解:

  • 为什么启动要这么抠延迟;
  • 为什么模式要这么早分流;
  • 为什么 query() 要做成 async generator;
  • 为什么整个系统要围绕 queryLoop 组织。

这才是看 Claude Code 前两章最该拿到的东西。

昨天以前首页

把 AI 协作搬进一间像素办公室,我做了个能真正跑起来的 Node 版看板

作者 兔子零1024
2026年4月4日 10:46

office-preview-1.jpg

office-preview-2.jpg

office-preview-3.jpg

office-preview-4.jpg 做工具的人应该都有这种感觉:最让人头疼的,往往不是任务太多,而是现场看不见。

终端里明明跑着好几个 Agent,屏幕上却只有一行一行的日志。谁在写东西,谁在查资料,谁在执行任务,谁已经掉线,很多时候只能靠猜。等你想回头捋一下昨天做了什么,窗口早就被新的输出刷没了。

所以我最近把自己很喜欢的 Star Office UI,认真做了一版更适合长期运行和实际接入的 Node 版本:Star Office UI Node

它不是那种“截图很好看、真用起来很费劲”的开源项目。它更像一块可以一直挂在副屏上的办公室看板,只不过这间办公室里坐着的,不是传统团队成员,而是一群正在协作的 AI 助手。

你会看到有人在工位上写东西,有人在服务器旁排查问题,有人处于待命状态,底下还放着“昨日小记”和访客列表。整个界面是像素风的,但它解决的问题其实很现实:把原本藏在日志和脚本里的协作状态,变成一眼就能读懂的现场。

这项目好看的地方,不只是“好看”

我喜欢这个项目,首先当然是因为它有记忆点。

像素办公室、书架、沙发、咖啡机、服务器区、小猫、状态气泡,这些元素一摆出来,整个页面不是冷冰冰的监控面板,而是一个很有存在感的工作现场。你甚至愿意把它一直开着,因为它看起来不像“又一个后台”,更像你团队的实时动态墙。

但真正让我想把它做出来的,不是美术,而是这个形式很适合 AI 协作。

AI Agent 最大的问题之一,从来不是“不会干活”,而是“干活的时候人看不明白”。有了这个看板以后,状态就不再只是 writingresearchingexecuting 这样的字符串,而是被放进一个直觉化的空间里。谁在忙、忙到哪一步、现场大概什么气氛,打开页面就知道。

这不是为了可爱,是为了可见。

这不是“又一个复刻”,而是更适合接进现有工作流

Star Office UI Node 是基于上游 Star-Office-UI 的理念和画面做的 Node / Express 实现。我尽量保留了原来的视觉体验和 HTTP 行为,但后端不是简单照着拼一遍,而是按“可以长期跑服务”的思路重新整理过。

如果你已经有现成的 Agent 脚本,尤其是 OpenClaw、龙虾这类工作流,接入成本会比较低。它提供了比较清楚的 join-agentagent-pushleave-agent 这一套接口,Agent 进来之后,状态就能持续推到看板上。谁上线了,谁在写,谁在同步,谁出问题了,页面会自己动起来。

而且这版我比较在意工程上的“省心”:

  • 用的是 Node 20 和 pnpm,工具链要求写得比较明确,少一点“你电脑能跑、我电脑跑不了”的玄学问题。
  • 服务支持 SIGTERM 和 SIGINT 的优雅退出,丢进 Docker 或 K8s 里也比较踏实。
  • 带 GET /health 和 GET /ready,做探针、做发布都顺手。
  • 主状态、多 Agent 列表、接入密钥都直接落 JSON,备份和挂载都很直观。

简单说,它不是一个只能演示的页面,而是一套真的能接进日常流程里的状态服务。

我很喜欢“昨日小记”这个细节

很多项目会把重点放在“现在发生了什么”,但我觉得“昨天留下了什么”也很重要。

Star Office UI Node 里有个我特别喜欢的小功能:昨日小记。它会去读 memory/ 目录里按日期命名的 Markdown,把昨天的记录提炼成简短内容,直接显示在界面里。

这件事看起来不大,但很有用。因为很多时候,协作不是断裂的。今天的任务,往往接在昨天的判断后面。一个看板如果只能告诉你“现在谁在忙”,那它只是监控;如果还能提醒你“昨天做到了哪”,它就更像一个有上下文的工作现场。

这种味道,我很喜欢。不是纯技术炫耀,而是真的在替持续协作考虑。

不同场景下,它都挺合适

如果你是一个人在折腾多 Agent 工作流,这个项目很适合挂在副屏上。写代码的时候抬头看一眼,就知道现在系统在忙什么。

如果你是小团队协作,它也很适合当作共享看板。尤其是远程协作时,大家不一定总在同一个聊天窗口里,但这个页面能让人迅速进入同一个上下文。

如果你只是单纯喜欢像素风、喜欢把工具做得有点意思,那它也值得一试。毕竟不是每个开源项目都愿意在“实用”之外,再多做一点审美上的坚持。

另外,它还做了四种界面风格:像素、柔和、夜青、纸本。默认是像素风,但你完全可以按自己的桌面环境来切。这个细节也很妙,它不是在强迫你接受一种风格,而是在告诉你:工具也可以有一点自己的气质。

如果你也想试试

项目开源在 GitHub:

github.com/wangmiaozer…

拉下来以后,直接跑下面几步就行:

 
git clone https://github.com/wangmiaozero/Star-Office-UI-Node.git
cd Star-Office-UI-Node
pnpm install
pnpm start

默认访问 http://127.0.0.1:18791

如果你最近也在折腾 AI Agent、多助手协作、状态看板,或者只是想把自己的工作流弄得更顺眼一点,我挺推荐你看看这个项目。

它最打动我的,不是“这是一个 AI 项目”,而是它终于让 AI 协作这件事,看起来像一个真实发生的现场了。

如果你喜欢,也欢迎给仓库点个 Star。

❌
❌