Skip to Content
设计Prompt Suggestion提示建议 (NES) 设计

提示建议 (NES) 设计

预测用户在 AI 完成回复后接下来会自然输入的内容,并在输入框中以幽灵文本的形式显示。

实现状态:prompt-suggestion-implementation.md。推测引擎:speculation-design.md

概述

提示建议(下一步建议 / NES)是一个简短的预测(2-12 个词),由 LLM 在每次 AI 回复后调用生成。它以幽灵文本的形式出现在输入提示框中。用户可以通过 Tab/Enter/右箭头接受,或通过输入内容将其取消。

架构

┌─────────────────────────────────────────────────────────────┐ │ AppContainer (CLI) │ │ │ │ Responding → Idle 过渡 │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ 守卫条件(11 类) │ │ │ │ settings, interactive, sdk, plan mode, dialogs, │ │ │ │ elicitation, API error │ │ │ └────────────────────┬────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ generatePromptSuggestion() │ │ │ │ │ │ │ │ ┌─── CacheSafeParams 可用? ───┐ │ │ │ │ │ │ │ │ │ │ ▼ 是 否 ▼ │ │ │ │ runForkedQuery() BaseLlmClient.generateJson() │ │ │ │ (缓存感知) (独立回退) │ │ │ │ │ │ │ │ ──── SUGGESTION_PROMPT ──── │ │ │ │ ──── 12 条过滤规则 ─────── │ │ │ │ ──── getFilterReason() ──── │ │ │ └────────────────────┬────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ FollowupController (框架无关) │ │ │ │ 300ms 延迟 → 显示为幽灵文本 │ │ │ │ │ │ │ │ Tab → 接受(填充输入) │ │ │ │ Enter → 接受并提交 │ │ │ │ Right → 接受(填充输入) │ │ │ │ Type → 取消 + 中止推测 │ │ │ └─────────────────────────────────────────────────────┘ │ │ │ │ ┌─────────────────────────────────────────────────────┐ │ │ │ Telemetry (PromptSuggestionEvent) │ │ │ │ outcome, accept_method, timing, similarity, │ │ │ │ keystroke, focus, suppression reason, prompt_id │ │ │ └─────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────┘

建议生成

LLM 提示

[建议模式:建议用户接下来可能自然输入的内容。] 首先:阅读助手最新消息的最后几行——那里通常会出现下一步提示、技巧和可操作建议。 然后检查用户最近的消息和原始请求。 你的任务是预测**他们会**输入什么——而不是你认为他们应该做什么。 测试标准:他们会觉得“我正要输入那个”吗? 优先级:如果助手的最后一条消息包含类似“提示:输入 X 来...”或“输入 X 来...”的提示, 则将 X 提取为建议。这些是明确的下一步提示。 示例: 助手说“提示:输入 post comments 来发布发现” → “post comments” 助手说“输入 /review 来开始” → “/review” 用户要求“修复 bug 并运行测试”,bug 已修复 → “run the tests” 写了代码后 → “try it out” 任务完成,明显的后续操作 → “commit this” 或 “push it” 格式:2-12 个词,匹配用户的风格。或者不返回任何内容。 仅回复建议本身,不要带引号或解释。

过滤规则(12 条)

规则被拦截的示例
done“done”
meta_text“nothing found”, “no suggestion”, “silence”
meta_wrapped“(silence)”, “[no suggestion]”
error_message“api error: 500”
prefixed_label“Suggestion: commit”
too_few_words“hmm”(但允许 “yes”, “commit”, “push” 等)
too_many_words> 12 个词
too_long>= 100 字符
multiple_sentences“Run tests. Then commit.”
has_formatting换行、Markdown 加粗
evaluative“looks good”, “thanks”(使用 \b 词边界)
ai_voice“Let me…”, “I’ll…”, “Here’s…”

守卫条件

AppContainer useEffect(代码中 13 项检查):

守卫检查条件
设置开关enableFollowupSuggestions
非交互模式config.isInteractive()
SDK 模式!config.getSdkMode()
流式传输过渡Responding → Idle(2 项检查)
API 错误(历史)historyManager.history[last]?.type !== 'error'
API 错误(待定)!pendingGeminiHistoryItems.some(type === 'error')
确认对话框shell + general + loop detection(3 项检查)
权限对话框isPermissionsDialogOpen
启发式询问settingInputRequests.length === 0
计划模式ApprovalMode.PLAN

generatePromptSuggestion() 内部:

守卫检查条件
早期对话modelTurns < 2

独立功能标志(不在守卫块中):

标志控制
enableCacheSharing是否使用分叉查询,或回退到 generateJson
enableSpeculation是否在建议显示时开始推测

状态管理

FollowupState

interface FollowupState { suggestion: string | null; isVisible: boolean; shownAt: number; // 用于遥测的时间戳 }

FollowupController

框架无关的控制器,由 CLI(Ink)和 WebUI(React)共享:

  • setSuggestion(text) — 延迟 300ms 显示,null 立即清除
  • accept(method) — 清除状态,通过微任务触发 onAccept,100ms 防抖锁
  • dismiss() — 清除状态,记录 ignored 遥测
  • clear() — 硬重置所有状态和定时器
  • Object.freeze(INITIAL_FOLLOWUP_STATE) 防止意外修改

键盘交互

CLIWebUI
Tab填充输入(不提交)填充输入(不提交)
Enter填充 + 提交填充 + 提交(使用 explicitText 参数)
右箭头填充输入(不提交)填充输入(不提交)
输入字符取消 + 中止推测取消
粘贴取消 + 中止推测取消

按键绑定说明

Tab 处理程序显式使用 key.name === 'tab'(而不是 ACCEPT_SUGGESTION 匹配器),因为 ACCEPT_SUGGESTION 也会匹配 Enter,而 Enter 需要进入 SUBMIT 处理程序。

遥测

PromptSuggestionEvent

字段类型描述
outcomeaccepted/ignored/suppressed最终结果
prompt_idstring默认:‘user_intent’
accept_methodtab/enter/right用户接受方式
time_to_accept_msnumber从显示到接受的时间
time_to_ignore_msnumber从显示到取消的时间
time_to_first_keystroke_msnumber显示期间到首次按键的时间
suggestion_lengthnumber字符数
similaritynumber接受时为 1.0,忽略时为 0.0
was_focused_when_shownboolean终端是否处于焦点
reasonstring对于被抑制的情况:过滤规则名称

SpeculationEvent

字段类型描述
outcomeaccepted/aborted/failed推测结果
turns_usednumberAPI 往返次数
files_writtennumberoverlay 中的文件数
tool_use_countnumber执行的工具数
duration_msnumber墙上时钟时间
boundary_typestring停止推测的原因
had_pipelined_suggestionboolean是否生成了下一个建议

功能标志与设置

设置类型默认值描述
enableFollowupSuggestionsbooleantrue提示建议的总开关
enableCacheSharingbooleantrue使用缓存感知的分叉查询
enableSpeculationbooleanfalse预测执行引擎
fastModel(顶层)string""所有后台任务的模型(空值 = 使用主模型)。通过 /model --fast 设置

内部 Prompt ID 过滤

后台操作使用专用的 prompt ID(utils/internalPromptIds.ts 中的 INTERNAL_PROMPT_IDS),以防止它们的 API 流量和工具调用出现在用户可见的 UI 中:

Prompt ID用途
prompt_suggestion建议生成
forked_query缓存感知的分叉查询
speculation推测引擎

应用过滤:

  • loggingContentGenerator — 对于内部 ID,跳过 logApiRequest 和 OpenAI 交互日志
  • logApiResponse / logApiError — 跳过 chatRecordingService.recordUiTelemetryEvent
  • logToolCall — 跳过 chatRecordingService.recordUiTelemetryEvent
  • uiTelemetryService.addEvent不过滤(确保 /stats token 追踪正常工作)

思维模式

所有后台任务路径都显式禁用了思维/推理模式(thinkingConfig: { includeThoughts: false }):

  • 分叉查询路径createForkedChat)— 在克隆的 generationConfig 中覆盖 thinkingConfig,涵盖建议生成和推测
  • BaseLlm 回退路径generateViaBaseLlm)— 每次请求的配置覆盖基础内容生成器的思维设置

这样做是安全的,因为:

  • 缓存前缀由 systemInstruction + tools + history 决定,而非 thinkingConfig — 缓存命中不受影响
  • 所有后端(Gemini、兼容 OpenAI 的服务、Anthropic)都通过省略 thinking 字段来处理 includeThoughts: false — 对于不支持思维功能的模型不会产生 API 错误
  • 建议生成和推测并不受益于推理 token
Last updated on