oh-my-pi 提交分析 Map 阶段提示词深度解析:从文件 Diff 到事实观察的提取管线
Map 阶段是 oh-my-pi(packages/coding-agent,一个 IDE 深度集成的 Coding Agent)自动提交分析 map-reduce 流水线中的第一环:它把一批文件 Diff 交给模型,只要求提取"有事实依据的观察",不要求分类、不要求归纳。本文以 map.md 为骨架,结合 map-reduce.ts、prompts.ts、markdown.ts 与 diff.ts 等源码,完整讲解该提示词的逐段语义、模板渲染机制、批次编排、输出解析与容错设计,帮助你理解并复用这套"先映射、后归约"的提交信息生成策略。
Map 阶段在提交分析流水线中的定位
oh-my-pi 的提交分析(conventional commit 生成)位于 packages/coding-agent/src/commit/conventional/,该目录下除 map.md 外,还维护了同一套提示词族:
- analysis.md:完整 Diff 的一次性分类(含 scope 判定规则、changelog 元数据);
- fast.md:轻量快速提交信息生成;
- map.md:map 阶段,逐文件提取事实观察;
- reduce.md:reduce 阶段,把观察合并为单一分类;
- summary.md、summary-rewrite.md:摘要生成与改写。
提示词族由 prompts.ts 统一加载,ConventionalPromptFamily 类型枚举了 analysis | fast | map | reduce | summary | summary-rewrite 六类,PROMPT_BY_FAMILY 通过 Vite 的 with { type: "text" } 将 .md 作为文本导入:
import mapPrompt from "./prompts/map.md" with { type: "text" };
renderConventionalPrompt(family, context) 是渲染入口:它定位模板中的 <!-- USER --> 分隔符,分隔符之前的部分作为 system 提示,之后的部分作为 user 模板,交给 prompt.render(Handlebars 风格模板引擎)以 context 填充后返回 { system, user }。对 map 模板而言,system 部分是角色定义与提取规则,user 部分则是 <files> 循环与可选的 <related_files> 块。
触发条件:什么时候进入 map-reduce 流程
并非所有提交都会走 map-reduce。判定函数 shouldUseMapReduce(diff, config) 位于 map-reduce.ts,它基于 config.ts 中的 ConventionalGenerationConfig 计算:
export function shouldUseMapReduce(diff: string, config: ConventionalGenerationConfig): boolean {
if (!config.mapReduceEnabled) return false;
let totalTokens = 0;
let hasIncludedFile = false;
for (const file of includedFiles(parsePromptDiff(diff), config)) {
hasIncludedFile = true;
const tokens = file.tokenEstimate();
if (tokens > MAX_FILE_TOKENS) return true; // 单文件超过 50_000 tokens
totalTokens += tokens;
if (totalTokens >= config.mapReduceThreshold) return true; // 累计达到阈值
}
return hasIncludedFile && totalTokens >= config.mapReduceThreshold;
}
关键阈值与默认值(见 config.ts 的 DEFAULT_CONVENTIONAL_GENERATION_CONFIG):
| 配置项 | 默认值 | 含义 |
|---|---|---|
mapReduceEnabled |
true |
是否启用 map-reduce 两阶段流程 |
mapReduceThreshold |
5_000 |
全部有效文件的 token 估算累计达到该值即启用 |
mapBatchTokenBudget |
16_000 |
单个 map 批次的 token 预算上限 |
excludedFiles |
锁文件清单 | 以 .endsWith 匹配后从输入中剔除(如 Cargo.lock、package-lock.json、bun.lock、uv.lock 等) |
单文件 token 估算由 ConventionalFileDiff.tokenEstimate() 提供(diff.ts):(header + content 的码点长度) / 4 向下取整、最少为 1,是一个确定性的四字符一 token 近似。测试 commit-conventional.test.ts 中通过 mapReduceThreshold: 1 之类的极小值来强制走 map-reduce 路径验证行为。
提示词逐段解读:角色、指令、范围、输出格式与校验
map.md 的 system 部分由五个 XML 风格的语义块构成,逐段含义如下。
角色声明(<role>)
<role>Expert code analyst extracting grounded observations from a batch of file diffs.</role>
把模型限定为"从一批文件 Diff 中提取有依据观察的资深代码分析师"。这里的关键词是 grounded(有依据):后续所有规则都在强化"只写 Diff 里看得见的事实"这一约束,为 reduce 阶段的可信合成打下基础。
提取指令(<instructions>)
指令块是 map 阶段的行为核心,逐条拆解:
- 只提取能被当前文件 Diff 支撑的事实观察,保持精确(
Extract only factual observations supported by each current file diff. Be precise.)——这是唯一性原则,观察必须落在当前文件的 Diff 上; <related_files>仅用于解析名称或引用,不得为这些文件添加观察(Use <related_files> only to resolve names or references; do not add observations about those files.)——related_files 只是上下文,不是观察来源;- 对
<files>中每个<file>,返回 0~5 条观察——给出数量上限,防止对单文件过度展开; - 使用"过去式动词 + 具体对象 + 可选目的"的句式——统一时态与句式,便于 reduce 阶段做字符串级归并;
- 每条观察控制在 100 字符以内;
- 覆盖该文件中有意义的变更,省略格式化、纯注释与 import 顺序调整——过滤低信号变更;
- 属于同一整体的相关编辑应合并,但不得猜测或过度概括;
- 对用户可见的行为变更(新能力、默认值变更、修复、移除)要明确且事实性地标注;
- 不得提及 commit 类型、scope、changelog 或任何 reduce 阶段的分类——map 与 reduce 职责严格分离,map 只产出原料。
范围界定(<scope>)
Include: functions, methods, types, API changes, behavior/logic changes,
error handling, performance, security.
Exclude: import reordering, whitespace/formatting, comment-only changes,
debug statements.
白名单(函数/方法/类型、API 变更、行为或逻辑变更、错误处理、性能、安全)与黑名单(import 重排、空白与格式、纯注释、调试语句)并用,让模型在"什么值得写"上有明确裁决标准。
输出格式(<output_format>)
map 阶段的输出是每文件一个 # 标题 + - 子弹列表的 Markdown:
# src/config.rs
- added TOML configuration loading
- changed default timeout to 30s
# src/main.rs
- changed CLI parsing to accept config paths
# src/empty.rs
配套规则:
- 每个输入文件恰好一个
#标题,path必须与<file path="...">中完全一致; - 观察以
-子弹列在标题下; - 必须覆盖每一个输入文件;某文件没有相关观察时,只输出其
#标题、不带任何子弹——这个"空标题"约定非常关键,它是后续解析器判定"文件已处理"的信号。
验证清单(<verification>)
三条自检:每条观察都能被该文件的 Diff 直接支撑;没有观察只依赖 <related_files>;没有重复、琐碎或分类导向的观察。模板末尾还有一句总纲:Observations only. Reduce phase handles classification and synthesis.(只产出观察,分类与综合由 reduce 阶段负责)。
模板变量与 Handlebars 渲染
map.md 的用户段位于 <!-- USER --> 之后,包含两个 Handlebars 块:
<files>
{{#each files}}
<file path="{{path}}">
{{diff}}
</file>
{{/each}}
</files>
{{#if context_header}}
<related_files>
{{ context_header }}
</related_files>
{{/if}}
渲染时注入两个变量:files(形如 { path, diff } 的对象数组)与可选的 context_header(related_files 文本)。在 map-reduce.ts 的 mapFileBatch 中可以看到实际装配:
const promptFiles = files.map(file => ({ path: file.filename, diff: renderFileDiffForBatch(file, budget) }));
const prompts = renderConventionalPrompt("map", { files: promptFiles, context_header: contextHeader });
renderFileDiffForBatch(file, budget) 会按批次 token 预算裁剪单个文件的 Diff:若文件 token 估算与字节数都在预算内则完整保留,否则克隆文件、调用 truncate 截断内容(保留头部 15 行、尾部 10 行,中间以 ... (truncated N lines) ... 占位,参见 diff.ts)。
related_files 上下文的构造逻辑
ContextHeaders 类(map-reduce.ts)负责为每个批次生成"同一次变更中的其他文件"概览,其核心是 inferFileDescription(map-reduce.ts)这个基于文件名与内容启发式的分类器:
- 文件名含
test→test file; - 含
prompt/system→prompt template; - 后缀
.md→documentation; - 含
config或后缀.toml/.yaml/.yml→configuration; - 含
error→error definitions;含type→type definitions; mod.rs/lib.rs→module exports;main.rs/main.go/main.py→entry point;- 内容含
class/def/fn→implementation;含struct/enum→type definitions;含async/await→async code;否则兜底source code。
headerForFiles(currentFiles) 取出不在当前批次中的其他文件,按 additions + deletions 行数降序排列,最多展示 MAX_CONTEXT_FILES = 20 个,超出时追加 ... and N more files 提示。特别地,当提交文件总数超过 100 时,直接退化为一行 (Large commit with N total files),避免超大提交生成巨型上下文。这些相关文件描述正是 map 提示词中"只用来解析名称或引用"的那部分上下文。
批次编排与并发执行
mapPhase(map-reduce.ts)是 map 阶段的运行时编排,值得注意的工程细节:
- 二进制文件短路:
isBinary的文件不进入模型调用,直接产出固定的Binary file changed.观察并保留其增删行数与状态; - 预算计算:
effectiveMapBudget(totalTokens, config.mapBatchTokenBudget)先按totalTokens * 5 / (16 * 4)估算理想批次预算,再夹在MIN_MAP_BATCH_TOKENS = 4_000与用户配置的mapBatchTokenBudget(默认 16_000)之间; - 贪心分批:
buildLlmFileBatches过滤掉二进制文件后,用buildBatchesForIndices做 token(预算 ×1)与字节(预算 ×4)双预算的贪心装箱;单文件超过预算时独占一个批次; - 并发度:
MAP_PHASE_CONCURRENCY = 16,mapWithConcurrency用固定数量 worker 的竞速模式(worker 循环取下一个 index)处理批次,保证长尾批次不被阻塞; - 进度反馈:每个批次调用
inference.complete时携带progressLabel(如Mapping batch 2/5 (3 files)…),toolName为create_file_observations,promptFamily为"map"。
输出解析:宽容的 Markdown 契约
map 提示词要求"不带 fences 输出",但模型输出往往不守规矩。解析器 parseFileObservationsMarkdown(markdown.ts)对多种形态做了宽容处理:
- JSON 形态:
{"files":[{path, observations}]}或纯数组均可解析,字段兼容path/file、observations/details; - Markdown 形态:逐行扫描,行首可匹配
#标题、file:/file =/`` 反引号包裹等变体(正则^(?:#+\s*)?(?:file\s*[:=-]\s*)??(.+?)?\s*:??$),且候选必须含/或.才被视为文件标题,避免把普通句子误判为路径;子弹行支持- * • – +以及有序列表1.`; - 解析结果会去掉每条观察末尾的句号(
stripTrailingPeriod),统一为无句号事实句。
随后 mapResponseToObservations(map-reduce.ts)把解析结果映射回文件顺序,匹配策略分三档:
- 候选路径与文件名完全相等;
- 文件名basename 唯一时按 basename 匹配;
- 最后按路径后缀匹配(
pathSuffixMatches)。
若模型因 max_tokens/length 停止且某文件无观察,会回填 Updated <basename>. 兜底观察;完全无匹配的文件同样回填兜底;批次结果全部返回后,任何缺失观察的文件会抛出 Missing map observation for <filename> 错误——保证 reduce 阶段拿到的观察集合与输入文件一一对应。
观察如何进入 reduce 阶段
runMapReduce(map-reduce.ts)在 map 完成后,调用 renderObservationsMarkdown 把观察渲染成 reduce 的输入:
# src/foo.rs (+12/-4)
- added retry logic for network calls
- changed default timeout to 30s
未修改文件会标注状态(added/deleted/renamed)与 +N/-N 行数(map-reduce.ts),然后连同 condenseStat 处理过的 git stat、scope 候选与类型定义一起注入 reduce.md 模板,由 reduce 阶段产出单一 # type(scope): summary 标题、3~6 条细节与可选的 Fixes: issue 引用。至此完成"map 提取事实观察 → reduce 综合分类"的完整闭环,最终分析结果会用于生成 conventional commit 消息。
小结与复用建议
map.md 的设计精髓可以提炼为三条原则:
- 职责分离:map 只做"逐文件、逐条、有依据"的事实提取(0~5 条/文件、≤100 字符/条、过去式句式),分类、scope、changelog 全部留给 reduce,从提示词层面杜绝模型在原料阶段就下结论;
- 契约优先:明确的
# 文件+- 子弹输出格式、空标题表示"无观察"的约定,配合 markdown.ts 的宽容解析与三档路径匹配,让输出稳定可机器消费; - 资源可控:token 估算驱动触发阈值(默认 5_000)、批预算(默认 16_000)、并发 16、单文件 50_000 token 上限与按优先级截断,保证超大提交也可控地完成分析。
如果你需要在自己的 Agent 流水线中复用这套模式,可以直接参考 map.md 的提示词结构,配合 map-reduce.ts 的分批与并发编排,以及 markdown.ts 的解析器;若要调整触发灵敏度,只需修改 config.ts 中 mapReduceThreshold、mapBatchTokenBudget 等设置。相关行为在 commit-conventional.test.ts 中有配套测试可参考。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.2 K634
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown300
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java101
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java60
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript60
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python280