跳到主要内容

数据截至 (上游 commit afe54827dd65)

03 · 上下文来源与可扩展性

本章讲什么: 模型看到的那一大坨 system prompt 是怎么拼出来的, 以及 Crush 用哪四种机制往里加东西:项目记忆文件、skills、MCP、LSP。


1. System prompt:一份 Go 模板

结构

主提示词是一个 434 行的 Go 模板文件 internal/agent/templates/coder.md.tpl, 用 text/template 渲染(internal/agent/prompt/prompt.go:82 Build)。

模板顶部是静态的行为规范(<critical_rules><communication_style> 等), 底部才是运行时注入的动态部分

注入内容模板变量来源
工作目录{{.WorkingDir}}配置的 cwd
是否 git 仓库{{.IsGitRepo}}检查 .git 是否存在
平台{{.Platform}}runtime.GOOS
日期{{.Date}}当前时间
git 状态摘要{{.GitStatus}}实跑 git 命令(分支、状态、近期提交)
已配置的 LSP{{.Config.LSP}}配置
可用 skills{{.AvailSkillXML}}skills 发现结果
项目记忆文件{{range .ContextFiles}}见 §2
全局记忆文件{{range .GlobalContextFiles}}见 §2

组装逻辑集中在 internal/agent/prompt/prompt.go:165 promptData。 git 信息是每次建 prompt 时真的跑 git 拿的getGitBranch / getGitStatusSummary / getGitRecentCommits,同文件 259-289 行),不是缓存的。

运行时还会再追加一段

模板渲染完不是终点。Run 里还会把所有已连接 MCP 服务器的 instructions 包进 <mcp-instructions> 标签追加到 system prompt 末尾(internal/agent/agent.go:666-678)。 所以同一个会话,MCP 连上前后模型看到的系统提示词是不一样的。


2. 项目记忆文件:认别家的门牌

Crush 默认会读一串「AI 指令文件」当上下文,清单写死在 internal/config/config.go:28 defaultContextPaths

生态文件
通用约定AGENTS.md / agents.md / Agents.md
ClaudeCLAUDE.mdCLAUDE.local.md
GeminiGEMINI.mdgemini.md
Copilot.github/copilot-instructions.md
Cursor.cursorrules.cursor/rules/
Crush 自己CRUSH.mdcrush.mdCrush.md 及各自的 .local.md

它读别家的文件,这是刻意的兼容策略——你已经为 Claude Code 或 Cursor 写好的项目说明, Crush 直接拿来用,不要求你再写一份。

全局层面还会读用户配置目录下的 CRUSH.md 和上一级的 AGENTS.mdinternal/config/load.go:546-552)。目录路径(如 .cursor/rules/)会被递归展开成文件列表 (internal/agent/prompt/prompt.go:110 processContextPath)。


3. Skills:只给目录,不给正文

它要解决的小问题

你想教 agent 一套专门流程(比如「本项目怎么发版」)。 把全文塞进 system prompt 太贵——用不到的时候也在烧 token。

思路:渐进式披露

Crush 的做法是目录与正文分离:system prompt 里只有一份「有哪些 skill、各自什么时候用、 正文在哪个文件」的清单,正文一个字都不进去。

怎么读这张图:方框内是注入 system prompt 的全部 skills 内容; 方框之下是模型自己走的后续动作,只有走到最后一步,SKILL.md 正文才进上下文。

system prompt 里只放一份清单
┌──────────────────────────────────────────────┐
│ <available_skills> │
│ <skill> │
│ <name>release-flow</name> │
│ <description>何时该用它</description> │
│ <location>/abs/path/SKILL.md</location> │
│ </skill> │
│ </available_skills> │
└──────────────────────────────────────────────┘

模型判断「这次用得上」

主动调 view 工具读 <location>

正文才进入上下文,同时被 Tracker 记一笔

XML 生成在 internal/skills/skills.go:299 ToPromptXML; 主提示词第 14 条规则明确要求「匹配到就必须先 view 它的 <location> 再动手, <description> 只是触发器,不许凭它臆测 skill 的行为」 (internal/agent/templates/coder.md.tpl<critical_rules> 第 14 条)。

来源与优先级

来源位置说明
内置internal/skills/builtin/crush-configcrush-hooksjqgo:embed 打进二进制(internal/skills/embed.go:23 DiscoverBuiltin
全局GlobalSkillsDirs()internal/config/load.go:1334加载配置时自动追加进 SkillsPathsinternal/config/load.go:598-603
项目ProjectSkillsDir(workingDir)internal/config/load.go:1380同上,追加在全局之后(internal/config/load.go:606

顺序即优先级:内置先进列表、用户 skill 后进,去重时后来者胜, 所以同名时用户 skill 覆盖内置,并打一条 warn 日志 (internal/agent/prompt/prompt.go:188-199;去重在 internal/skills/skills.go:365 Deduplicate)。 带 disable-model-invocation 的 skill 不进清单——它只能由用户手动触发。

加载追踪

skills.Trackerinternal/skills/tracker.go:15)记录本次会话哪些 skill 被真正读过。 view 工具读到 skill 文件时调 MarkLoadedinternal/agent/tools/view.go:270), 每轮结束打一条使用日志(internal/agent/coordinator.go:1538 logTurnSkillUsage)。


4. MCP:异步连接,不拖慢首个 prompt

生命周期

workspace 启动

mcp.Initialize(ctx, ...) ← 每个服务器一个 goroutine,互不阻塞

┌─────┴──────┐
│ │
连上 连不上/需授权
注册工具 置为对应状态,发事件
发 EventToolsListChanged

Coordinator 收到 → SetTools 换掉当前工具表

关键取舍在 internal/agent/coordinator.go:228-246 的一大段注释里说得很清楚:

场景是否等 MCP 初始化完理由
交互模式不等曾经因为等最慢的服务器超时,导致第一条消息卡住整个 TUI
非交互(crush runWaitForInit只有一次机会拿工具表,缺工具就等于任务失败

交互模式下漏掉的 MCP 工具会在后续轮次自动补上——因为 PrepareStep 每一步都重新 a.tools.Copy() 取最新工具表(internal/agent/agent.go:814)。

传输与鉴权

支持 stdio / http / sse 三种传输(internal/config/config.go:185 MCPType)。 HTTP 类支持 OAuth:待授权的服务器进入 pending 状态,UI 给出授权链接, 授权完成再重连(internal/agent/tools/mcp/init.go:400 PendingAuthMCPs:426 BeginAuth)。

权限

MCP 工具默认全部需要权限确认internal/agent/tools/mcp-tools.go:99 Tool.Run), 只有 Docker MCP 的几个管理类工具在白名单里免确认 (internal/agent/tools/mcp-tools.go:15 whitelistDockerTools)。

agent 配置还能按服务器、按工具名做白名单(AllowedMCP), 过滤逻辑在 internal/agent/coordinator.go:768-789


5. LSP:按需自动拉起

思路

不是启动时把所有语言服务器都拉起来,而是模型碰到某个文件时才尝试internal/lsp/manager.go:104 Start)。

判断能不能自动启动,走一串由便宜到昂贵的检查(internal/lsp/manager.go:253 canAutoStart):

命令名太泛?(node/python/java/npx/deno...)
├── 是 ──► 跳过(除非用户显式配置)
└── 否
文件类型 / 根标记文件匹配吗? ← 便宜,先做
├── 否 ──► 跳过
└── 是
最近判定过"没装"吗? ← 有冷却期,避免反复找
├── 是 ──► 跳过
└── 否
PATH 里找得到命令吗? ← 最贵,最后做
├── 否 ──► 标记不可用,跳过
└── 是 ──► 启动

排序的理由源码里直接写死在注释里(internal/lsp/manager.go:262-267,真实源码):

// Filtering by file type is cheap and usually rejects a server before the
// root marker check. Do both before searching PATH, which can require a stat
// for every directory in PATH for every bundled server.
if !handles(server, filePath, workDir) {
return false
}

两个细节值得学:

  • 顺序是按代价排的:查 PATH 可能要对每个 PATH 目录、每个内置服务器各做一次 stat, 所以排在文件类型过滤之后(上面这段注释)。
  • 「太泛的命令」黑名单skipAutoStartCommandsinternal/lsp/manager.go:123): nodepythonjavadenodotnet 这类命令谁机器上都有,凭它存在就拉起服务器几乎必错, 所以要求用户显式配置。

还有一条隐含边界:Start 只处理工作目录之内的文件 (internal/lsp/manager.go:108fsext.HasPrefix 检查)。


6. 配置:分层合并 + 可执行的 crushrc

6.1 分层顺序

后加载的覆盖先加载的(internal/config/load.go:915 lookupConfigs):

系统级配置

全局用户配置(crush.json 与同目录 crushrc)

全局数据目录 JSON(机器状态,永不当脚本执行)

从工作目录向上找到的项目配置(离 cwd 越近优先级越高)

workspace 配置(.crush 目录里的,优先级最高)

同一目录内的优先级:.crushrc > crushrc > .crush.json > crush.jsoninternal/config/load.go:927-936configNames 及其上方注释)。 向上查找有边界(internal/config/load.go:1315 projectBoundary),不会一路找到根目录。

6.2 crushrc:配置是一段脚本

这是 Crush 一个挺特别的设计:crushrc 不是 JSON,而是一段真的会被执行的 shell 脚本internal/shellconfig/load.go:33 LoadShellConfig)。

crushrc 源码
│ 用和 bash 工具同一个内嵌解释器跑

脚本调用 provider / model / mcp 等"配置 builtin"
│ 按执行顺序改一个 ConfigBuilder

builder 序列化成 JSON


和其它配置文件一起走正常合并流程

好处是配置里能用 $VAR$(command)source、条件分支—— 比如按 CRUSH_VERSION 做特性检测(脚本里能读到这个变量,internal/shellconfig/load.go:46)。

代价是执行任意代码的风险,Crush 用两道约束控制:

  1. 数据目录的 JSON 永远不当脚本执行——注释里点名说它是「机器持有的可写状态」 (internal/config/load.go:916-919 注释)。
  2. 30 秒硬超时internal/shellconfig/load.go:20 loadTimeout)。 因为配置加载发生在启动关键路径上、还握着配置存储的写锁,脚本卡住会锁死整个程序。

同一目录同时存在 JSON 和 crushrc 且顶层键有重叠时会打 warn;不重叠则认为是有意共存, 不打扰用户(internal/config/load.go:1007-1017)。

6.3 环境自适应的默认值

配置加载最后还会按环境调整(internal/config/load.go:88-101):

检测到调整
不在 git worktree 内限制文件遍历:深度 2、条目 100
Apple Terminal打开透明模式

第一条是防呆:在非仓库目录(比如家目录)启动时,glob/补全不至于把整块硬盘扫一遍。


7. 代码地图

主题文件路径符号名
主提示词模板internal/agent/templates/coder.md.tpl(模板)
提示词组装internal/agent/prompt/prompt.goPrompt.BuildpromptDataloadContextFilesprocessContextPath
各家记忆文件清单internal/config/config.godefaultContextPaths
Skills 数据结构与 XMLinternal/skills/skills.goSkillDiscoverToPromptXMLDeduplicateFilter
内置 Skillsinternal/skills/embed.goDiscoverBuiltin
Skills 目录来源internal/config/load.goGlobalSkillsDirsProjectSkillsDir
Skills 使用追踪internal/skills/tracker.goTracker.MarkLoaded
MCP 初始化internal/agent/tools/mcp/init.goInitializeWaitForInitPendingAuthMCPsBeginAuth
MCP 工具包装internal/agent/tools/mcp-tools.goGetMCPToolsTool.RunwhitelistDockerTools
LSP 自动启动internal/lsp/manager.goManager.StartstartServercanAutoStartskipAutoStartCommands
配置分层internal/config/load.goLoadlookupConfigsprojectBoundary
可执行 crushrcinternal/shellconfig/load.goLoadShellConfigloadTimeout