# Step 27: 输出格式控制

一句话导读:让用户用 /style 在「简洁/详细/极简」间切换 AI 的表达风格——而背后不写一行输出后处理,只是把对应的几行指令拼进 system prompt,让模型自己调整说话方式。


# 一、这一步做了什么(What)

新增 utils/styles.ts,把「输出风格」变成可切换的设置:

  1. 三种内置风格 concise / detailed / minimal,每种对应一组 system prompt 指令;
  2. getStyle() 做容错解析——用户输错也能命中最接近的风格;
  3. prompt.ts 的 buildSystemPrompt 接受 styleId,把 STYLE_INSTRUCTIONS[style] 追加到提示词末尾;
  4. /style 命令查看/切换,切换后通过 step26 的持久化写回配置,重启依旧生效。

关键:风格是通过 system prompt 控制模型行为,而非用代码裁剪模型输出。


# 二、面试官视角:为什么这样做?(Why)

面试题:想让 AI 输出更简洁,为什么不在拿到回复后用代码截断/正则清理,而要去改 system prompt?

因为后处理是跟模型对着干,提示词是让模型顺着做——前者脆弱,后者可靠。

方案 简洁化怎么实现 问题
输出后处理 正则删段落、截断字数 会切断代码块、破坏 markdown、误删关键信息,规则永远补不完
改 system prompt 加一句"保持简洁" 模型从生成阶段就往简洁走,语义完整、无副作用

核心洞察:行为控制优先用提示词,而非后处理。你想要的「简洁」不是「字少」,而是「信息密度高、该省的省」——这是语义判断,只有模型自己能做好。用正则截断只会得到「被砍断的啰嗦」。把控制权交回模型,让它在生成时就按风格组织语言,输出才是完整且符合预期的。附带好处是零新增存储:风格直接挂在已有的 system prompt 构建器(step21)和配置持久化(step26)上,没有一处需要新的基础设施。


# 三、原理:它是怎么工作的(How)

# 风格定义与容错解析(真实代码)

export type OutputStyle = 'concise' | 'detailed' | 'minimal';

export const STYLE_INSTRUCTIONS: Record<string, string[]> = {
  concise:  ['- Keep responses brief but informative', '- Summarize tool results concisely'],
  detailed: ['- Provide thorough explanations', '- Show full command output', '- Explain reasoning step by step'],
  minimal:  ['- Be extremely brief', '- Show only essential results', '- Skip explanations unless asked'],
};

export function getStyle(id: string): string {
  if (id === 'concise' || id === 'detailed' || id === 'minimal') return id;   // 精确命中
  if (id.includes('det') || id.includes('full'))  return 'detailed';          // 模糊兜底
  if (id.includes('min') || id.includes('short')) return 'minimal';
  return 'concise';                                                            // 最终默认
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

getStyle 是一个三级容错漏斗:先试精确匹配,再试子串模糊匹配(det/full→detailed,min/short→minimal),全不中就回落到 concise。用户输 "detail"、"fulloutput"、"short" 都能命中,永远不会因为拼错而报错——这就是「永远返回一个合法风格」的设计。

# 风格如何注入 system prompt(真实代码)

export function buildSystemPrompt(registry: ToolRegistry, styleId?: string): string {
  const style = getStyle(styleId || "concise");
  return [
    "You are Claude, a helpful coding assistant running in a terminal.",
    // ... 工具使用指南、工作流程 ...
    "=== 输出格式 ===",
    ...STYLE_INSTRUCTIONS[style],        // ★ 把风格指令展开拼到末尾
  ].join("\n");
}
1
2
3
4
5
6
7
8
9

...STYLE_INSTRUCTIONS[style] 用展开运算符把那一组指令行摊进 prompt 数组的末尾。风格切换的全部「魔法」就是这一行——换 style,换的是拼进去的那几行文字,模型读到不同指令就表现出不同风格。

# 数据流

/style minimal
  ↓
getStyle("minimal") → "minimal"
  ↓
settings.style = "minimal"  →  saveSettings()   // step26 持久化
  ↓
下一轮 buildSystemPrompt(registry, "minimal")
  ↓
system prompt 末尾拼入 minimal 的 3 行指令
  ↓
模型据此生成极简输出
1
2
3
4
5
6
7
8
9
10
11

# 设计细节表

细节 做法 为什么
提示词控制而非后处理 指令拼进 system prompt 让模型语义级调整,不破坏结构
三级容错解析 精确→模糊→默认 用户拼错也命中,降低记忆负担
展开注入 ...STYLE_INSTRUCTIONS[style] 一行完成风格插入,无分支
复用持久化 挂在 step26 上写回配置 零新增存储,重启生效

# 四、深入追问(面试常见 follow-up)

Q:getStyle 为什么设计成「永远返回合法值」而不是对非法输入报错? A:因为这是面向终端用户的命令,容错比严格更重要。用户记不住是 "detailed" 还是 "detail",输错就报错会很挫败。三级漏斗保证任何输入都落到一个合理风格(最差回 concise),命令永远「能用」。这体现的是为人类输入设计要宽进严出。

Q:把风格塞进 system prompt,模型会不会不听、还是啰嗦? A:有可能——提示词是「引导」不是「强制」,模型偶尔不完全遵守。这是提示词方案的固有软肋。但相比后处理会「确定地」破坏输出结构(切断代码块),提示词方案的「偶尔不听」是更能接受的失败模式:最坏情况只是这次没那么简洁,而不是输出被弄坏。

Q:风格指令是加在 system prompt 末尾,位置有讲究吗? A:有。放末尾(靠近对话)通常让指令更「新鲜」、更被重视;而且放在工具指南、工作流程之后,语义上是「在你会用工具、懂流程的基础上,再按这个风格表达」,层次清晰。若把风格插在最前面,容易被后面大段工具说明冲淡。

Q:切换风格后,已经在历史里的旧回复会变吗? A:不会。buildSystemPrompt 是每轮请求时用当前 style 重新构建的,只影响之后的生成。历史消息是既成事实,风格切换是「从现在起这么说话」,不回溯。这也符合用户直觉。


# 五、踩坑 / 设计权衡

  • 提示词引导 vs 强约束:提示词方案简单、不破坏输出,但无法 100% 保证遵守。要强约束得配合输出校验+重试,成本高。学习版选前者,够用。
  • 固定三档 vs 自定义风格:我们内置三种。真实 Claude Code 支持 output-styles 目录下的自定义风格文件,用户能写自己的风格模板——更灵活,我们这版是简化。
  • 风格 vs 内容耦合:风格只改「怎么说」,不该改「说什么」。指令措辞要小心,别写成会改变实际行为的内容。

# 六、与真实源码的对照

我们的实现 Claude Code 源码
utils/styles.ts 三种内置风格 outputStyle 设置 + output-styles 目录(支持自定义风格文件)
getStyle 三级容错解析 更完整的风格注册/查找机制
指令拼进 system prompt 同样是提示词驱动,非后处理
挂 step26 持久化 风格作为持久化设置项

# 七、一句话总结

输出风格控制 = 用提示词而非后处理来改表达:三种风格各对应几行 system prompt 指令,getStyle 三级容错保证输入永远命中,一行展开注入完成切换,复用已有的 prompt 构建器和配置持久化——「行为控制优先提示词」这一原则的最小而典型的落地。

# 下一节预告

Phase 3 到此收官。step28 不写新代码,翻开真实 claude-code-src/src/,把我们 11–27 步实现的 17 个模块逐项对回源码,看清「简化版」离「真身」还差多远。