跳到主要内容

Crush — 架构与原理

30 秒导读: Crush 是 Charm 出的「终端里的编程搭档」——你在 TUI 里打字,它调用你选的任意 LLM,用一套工具(读文件、改文件、跑 shell、查 LSP)真的去改你的代码库。它的工程精华不在「调模型」,而在会话的生命周期管理:一个会话可以被并发提交多条 prompt、可以随时按 Esc 取消、上下文超长时会自动摘要再续跑——而所有这些路径都必须收敛到「恰好一个终结事件」,否则 crush run 这样的非交互客户端会挂死或提前退出。

1. 这是什么(零基础也能懂)

一句话定义: Crush 是一个跑在终端里的 AI 编码助手——你用自然语言描述要做的事,它边「想」边调用工具(看代码、改文件、执行命令)把事做完。

解决什么问题 / 给谁用: 假设你在终端里干活,不想切到 IDE 或网页。你打开 Crush,说「给登录函数加错误处理」,它会自己搜索代码、读相关文件、改文件、跑测试,然后告诉你「Done」。它是给习惯命令行的工程师用的,接入你自己选的模型(Anthropic / OpenAI / Google / 本地模型都行)。

它能做什么:

  • 多模型:任意 OpenAI / Anthropic 兼容 API,会话中途可切换且保留上下文。
  • 基于会话:一个项目可以有多个工作会话,各自独立的对话历史。
  • 工具齐全:view / edit / multiedit / write(文件),bash(命令),grep / glob / ls(搜索),fetch / web_search(联网),diagnostics / references(LSP)。
  • LSP 增强:改完文件自动跑 LSP 诊断,把报错回灌给模型。
  • 可扩展:通过 MCP(Model Context Protocol)接外部工具;通过 skills 注入领域知识。
  • 子 agent:主 agent 可以派一个只读的 agent 子 agent 去并行搜集上下文。

用起来什么样: 一段最小交互——

$ crush
> 给 internal/auth/login.go 的 Login 函数加上输入校验

[Crush 调 grep 找 Login → view 读文件 → edit 改文件 → bash 跑 go test]
Done

非交互模式也行,适合脚本和 CI:

$ crush run "修复 flaky 测试 TestUpload"

一句话直觉/类比: 把一个会话想成一条不断追加的「消息流」(用户说一句、助手说一句、工具吐一段结果……全部按时间落进 SQLite),把agent 循环想成一个「读流 → 让模型续写 → 模型要调工具就执行 → 工具结果接回流里 → 再让模型续写」的转盘,转到模型说「我说完了」为止。

本节到此为止,不碰底层。

2. 顶层全景(它大概怎么转)

Crush 在内部分成清晰的几层:入口/UI → Coordinator(协调器)→ SessionAgent(会话 agent,核心循环)→ Tools(工具)→ 持久化(消息 / 会话 / 文件历史)。外部能力(LLM 经 fantasy 库、模型元数据经 catwalk、语言服务经 LSP、外部工具经 MCP)挂在边上。

用户在 TUI 里输入 prompt(或 `crush run`)


┌──────────────────────────────────────────────┐
│ Coordinator (internal/agent/coordinator.go) │
│ · 选模型 / 拼 provider options / OAuth 刷新 │
│ · 失败重试 + 把多次尝试合并成一个终结事件 │
└───────────────┬────────────────────────────────┘
│ SessionAgentCall{SessionID, Prompt, ...}

┌──────────────────────────────────────────────┐
│ SessionAgent.Run (internal/agent/agent.go) │ ← 全书核心
│ · accept/queue/cancel 三态握手(防并发) │
│ · agent.Stream(...) 流式循环 + 一堆 On* 回调 │
│ · 每个增量写进消息流;每步存 usage/cost │
│ · 超长则 Summarize 再续;循环则 loop-detect 停 │
└───────┬───────────────────────┬─────────────────┘
│ 模型要调工具 │ 落库 / 事件
▼ ▼
┌──────────────────┐ ┌─────────────────────────────┐
│ Tools │ │ message.Service(SQLite) │
│ edit/bash/grep/… │ │ session.Service / history │
│ · 权限闸门 │ │ pubsub: Notification / │
│ · LSP 诊断回灌 │ │ RunComplete │
└──────────────────┘ └─────────────────────────────┘

部件一句话职责:

部件干什么在哪个文件
Coordinator把「用户/HTTP 一次提交」翻译成 SessionAgentCall,选模型、拼 provider 选项、做未授权重试、合并终结事件internal/agent/coordinator.go
SessionAgent核心:跑流式循环、管会话并发(accept/queue/cancel)、自动摘要、把流式增量落库internal/agent/agent.go(sessionAgent.Run)
fantasy.Agent第三方库:封装「多步工具调用循环」与各 provider 的流式协议,Crush 通过 On* 回调挂钩charm.land/fantasy(外部依赖)
catwalk第三方库:模型元数据(上下文窗口、单价、是否支持图片/推理)charm.land/catwalk(外部依赖)
Tools每个工具一个 fantasy.AgentTool:执行前过权限闸门,改文件后回灌 LSP 诊断internal/agent/tools/*.go
permission.Service串行化的权限请求:发事件给 UI,阻塞等用户允许/拒绝;支持会话级记忆与 allowlistinternal/permission/permission.go
message.Service消息流的增删改查 + 防抖式流式更新 + 终结时 flushinternal/message/
lsp.Manager按文件类型懒启动 LSP 客户端,提供诊断与引用internal/lsp/manager.go

主线走一遍(高层,不进代码):

  1. 用户在 TUI 输入 prompt,或 crush run 经 HTTP 进来。
  2. Coordinator.run 刷新模型配置、拼好 provider 选项,构造一个 SessionAgentCall,调 SessionAgent.Run
  3. Run 先做并发握手(这个会话现在空闲?忙?刚被取消?),决定立即跑 / 排队 / 当场取消
  4. 若立即跑:把用户消息写进消息流,调 fantasyagent.Stream。模型边吐 token,Crush 的 OnTextDelta / OnToolCall / OnToolResult 等回调把每一片增量实时写进消息流(SQLite),UI 通过 pubsub 订阅看到。
  5. 模型要调工具时,fantasy 调对应 AgentTool:先过权限闸门,执行,改文件类工具再跑 LSP 诊断,把结果接回流。
  6. 循环直到模型说「结束」(或上下文超长触发摘要、或检测到工具调用死循环而停)。
  7. Run 退出前 flush 所有缓冲的消息更新,发出唯一RunComplete 事件——非交互客户端就靠它知道「这一轮真的完了」。

3. 怎么往下读(阅读地图)

建议顺序(由浅入深):

  1. 01-agent-loop.md — 先看一条 prompt 怎样走完那个流式循环:agent.Stream 的各个 On* 回调分别在干嘛、增量怎么落库、一步结束时怎么记 usage。这是理解一切的地基。
  2. 02-concurrency.md — Crush 最硬核、代码注释最长的部分:同一个会话被并发提交/取消时的 accept/queue/cancel 三态握手,以及为什么「恰好一个 RunComplete」这么难。
  3. 03-tools-and-safety.md — 工具是 agent 的手脚:权限闸门怎么挡危险操作、bash 怎么黑名单、edit 怎么强制「先读后改」、LSP 诊断怎么回灌。
  4. 04-context-management.md — 上下文工程:自动摘要的触发与续跑、孤儿工具调用的修复(防止会话被永久锁死)、prompt 组装与 Anthropic 缓存。
  5. 05-cleverness-and-map.md — 提炼可借鉴的巧思、诚实列边界、和兄弟项目横向对比,最后给一张可 grep 的代码地图。

锚点提醒:所有引用 as-of commit f75435a2ac2e69cf4d64ad15bf5d84f284cdecd6。引用都带真实符号名(函数/类型),行号失效时可凭符号 grep。