Prompt Suggestions:首轮引导与停轮后下一句预测
在 Claude Code 的交互设计中,Prompt suggestions 并非单一的静态文案,而是一个结合了项目历史缓存与停轮后实时预测的双层提示机制。它通过在输入框中渲染“幽灵文本(Ghost Text)”来引导用户高效操作。
一句话讲清楚
Prompt suggestions 在 Claude Code 里其实由两层性质完全不同的机制构成。
第一层是 首轮占位引导(First-turn Placeholder)。当你刚开启一个全新会话时,输入框里出现的 Try "..." 本质上是基于项目历史的静态推荐。第二层是 停轮后实时预测(Post-turn Prediction)。每当 Claude 完成一轮推理,运行时会另外启动一个轻量级的 Fork Agent,利用此时此刻的 Prompt Cache 去预测“用户下一步最可能问什么”,并作为幽灵文本推送到输入框。
从源码看实现
整个提示系统的实现流程涵盖了从背景探测到实时预测的多个环节:
项目级示例抽取(Example Commands): 在
claude-code-opensource/src/utils/exampleCommands.ts中,系统会通过 Git 历史抽取最近高频修改的文件路径。它会过滤掉 lockfile、构建产物等无意义路径,拼装成how does X work?或refactor X模板存入项目配置。这决定了你看到的“第一屏”占位文案。缓存安全快照(Cache-Safe Snapshotting): 在
claude-code-opensource/src/query/stopHooks.ts中,主线程在生成结束时会先行保存lastCacheSafeParams。这是为了确保后续的预测 Fork 能复用和主线程完全一致的上下文前缀,从而极致节省预测所需的 Token 成本。预测性 Fork 代理(Speculative Prediction): 在
claude-code-opensource/src/services/PromptSuggestion/promptSuggestion.ts中,若环境支持且缓存命中预估(Cache Cold Check)通过,系统会通过runForkedAgent启动预测。为了确保不产生副作用,预测 Agent 会被设置为:- 禁工具态(Tools Denied):不允许执行任何工具,只进行文本推理。
- 跳过转录(Skip Transcript):预测过程不写入本地历史日志。
- 禁止缓存写(Skip Cache Write):预测产生的 Suffix 绝不污染后续的主会话缓存。
幽灵文本渲染与竞争(Rendering & Conflict): 预测结果通过
AppState.promptSuggestion暴露。在claude-code-opensource/src/components/PromptInput/PromptInput.tsx中,suggestion 必须与命令补全、文件建议等其他自动完成机制竞争显示位。只有当输入框为空且没有更高优先级的补全项时,Ghost Text 才会浮现。
限制与陷阱
- 首轮与后轮的区别:首屏
Try "..."是静态缓存,不耗费即时 Token;而后轮的 suggestion 是由模型实时生成的,依赖 Prompt Cache 降低成本。 - 抑制条件(Suppression):在 Plan Mode(计划模式)、等待权限审批、或者上一轮发生 API 错误时,预测机制会被主动抑制,以防干扰用户的核心决策。
- 成本敏感(Cache Cold):如果父请求的上下文没有被成功缓存,系统通常会判定为
cache_cold并拒绝生成建议,宁愿不给建议也不愿产生额外的冷启动开销。 - 多重接受行为:除了文档标注的
Tab键接受外,源码实现中还支持:右箭头键接受、以及在空输入状态下按Enter触发“接受并立即提交”。
继续探索
- 如果你想了解 suggestion 预测结果如何提前触发下一步响应的热启动,请看
src/services/PromptSuggestion/speculation.ts(暂未包含在核心文档)。 - 如果你想了解输入框中其他灰字建议如何与 suggestion 竞争,请看 输入模式与交互细节:按键绑定与输入逻辑。
- 如果你想对比轻量 Fork 与真正的 Subagent 的差异,请看 子代理机制:Subagent 的架构与治理。
源码锚点
claude-code-opensource/src/services/PromptSuggestion/promptSuggestion.ts— 开关判断、预测 Agent 配置与过滤规则。
📄 src/services/PromptSuggestion/promptSuggestion.ts — 开关判断、预测 Agent 配置与过滤规则。
runForkedAgent,
} from '../../utils/forkedAgent.js'
import type { REPLHookContext } from '../../utils/hooks/postSamplingHooks.js'claude-code-opensource/src/query/stopHooks.ts— 保存缓存安全参数并触发预测任务的入口。
📄 src/query/stopHooks.ts — 保存缓存安全参数并触发预测任务的入口。
const extractMemoriesModule = feature('EXTRACT_MEMORIES')
? (require('../services/extractMemories/extractMemories.js') as typeof import('../services/extractMemories/extractMemories.js'))
: null
const jobClassifierModule = feature('TEMPLATES')
? (require('../jobs/classifier.js') as typeof import('../jobs/classifier.js'))
: null
/* eslint-enable @typescript-eslint/no-require-imports */
import type { QuerySource } from '../constants/querySource.js'claude-code-opensource/src/utils/exampleCommands.ts— 基于 Git 历史生成首轮占位建议的算法。
📄 src/utils/exampleCommands.ts — 基于 Git 历史生成首轮占位建议的算法。
if (!(await getIsGit())) return []
try {
// Collect frequently-modified files, preferring the user's own commits.
const userEmail = await getGitEmail()
const logArgs = [
'log',
'-n',
'1000',
'--pretty=format:',
'--name-only',
'--diff-filter=M',
]
const counts = new Map<string, number>()
const tallyInto = (stdout: string) => {
for (const line of stdout.split('\n')) {
const f = line.trim()
if (f) counts.set(f, (counts.get(f) ?? 0) + 1)
}
}
if (userEmail) {
const { stdout } = await execFileNoThrowWithCwd(
'git',
[...logArgs, `--author=${userEmail}`],
{ cwd: getCwd() },
)
tallyInto(stdout)claude-code-opensource/src/components/PromptInput/usePromptInputPlaceholder.ts— 占位引导的渲染逻辑。
📄 src/components/PromptInput/usePromptInputPlaceholder.ts — 占位引导的渲染逻辑。
const proactiveModule =
feature('PROACTIVE') || feature('KAIROS')
? require('../../proactive/index.js')
: null
type Props = {
input: string
submitCount: number
viewingAgentName?: string
}claude-code-opensource/src/hooks/usePromptSuggestion.ts— 建议的显示、接受与忽略状态 Telemetry 记账。
📄 src/hooks/usePromptSuggestion.ts — 建议的显示、接受与忽略状态 Telemetry 记账。
type Props = {
inputValue: string
isAssistantResponding: boolean
}