reactive-resume 的 AI 对话系统提示词设计:JSON Patch 提案制简历编辑助手全解析
本篇以 chat-system.md 为核心,拆解 reactive-resume 中 AI 对话编辑简历的系统提示词(System Prompt)设计:它以“只允许通过 propose_resume_patches 工具发起 JSON Patch(RFC 6902)提案、用户逐条审批后才生效”为核心机制。读完本文,你将掌握这套“提示词 + 工具 Schema + 服务端校验 + 人工审批”四层防线的完整实现,并能理解从提示词模板加载、动态数据注入、工具调用、提案规范化到补丁校验回滚的全链路调用关系。
一、系统提示词的定位与加载机制
chat-system.md 定义了 AI 助手在“对话式修改简历”场景下的角色与行为契约,其首句即明确了整体设计立场:
You are a resume editing assistant that must propose resume data changes only through JSON Patch (RFC 6902) proposal tool calls.
也就是说,模型被约束为“只提建议、不做决定”的角色:任何数据变更都必须走 propose_resume_patches 工具调用,由用户审查后应用。这是 reactive-resume AI 功能中“人在回路(Human-in-the-loop)”安全模型的关键一环。
该模板在 packages/ai/src/prompts.ts 中通过 readPrompt 在模块加载时从磁盘读取并导出为 chatSystemPromptTemplate:
// packages/ai/src/prompts.ts
const readPrompt = (filename: string) => {
return readFileSync(new URL(`./prompts/${filename}`, import.meta.url), "utf-8");
};
const chatSystemPromptTemplate = readPrompt("chat-system.md");
与同目录下的解析类提示词(如 parser-system.md 通过变量替换生成 PDF/DOCX 两个变体)不同,chat-system.md 只有一个 {{RESUME_DATA}} 占位符,由 API 层在运行时注入当前简历数据(见第四节)。这种“Markdown 模板 + 运行期变量替换”的组织方式,让提示词可以独立于代码评审、版本化和测试——prompts.test.ts 就断言了模板内容包含 "resume" 等基本约束。
二、系统提示词全文核心内容详解
以下按原文档的章节顺序,完整继承并逐节展开。
2.1 Objective:目标与最小化编辑原则
原文档 Objective 部分给出三条目标:
- 帮助用户改进简历内容与结构(Help the user improve resume content and structure);
- 通过
propose_resume_patches工具安全、最小化地提出编辑(Propose edits safely and minimally); - 所有提案在应用前都由用户审查(The user reviews every proposal before anything is applied)。
“最小化编辑”这一原则并非口号,它贯穿到了工具层:后文的 operations 数组支持把多个操作聚合为一个提案,而提示词明确要求“生成满足请求所需的最小操作集合”(Hard Constraint 2),避免模型用整对象替换的方式重写无关字段。
2.2 Allowed Inputs:输入边界
原文档明确模型可见的输入只有两类:
- 对话中的用户指令(User instructions in conversation);
- 下方提供的当前简历 JSON 状态(Current resume JSON state provided below)。
从源码结构看,这一边界是可信的:API 层的 chat 处理器只把 messages(对话历史)和 resumeData(从数据库按 resumeId 查出的简历)传给模型,见 service.ts 中的 buildChatSystemPrompt 与 streamText 调用。模型接触不到用户档案、应用追踪等其它数据,提示词层面的“Allowed Inputs”与代码层面的实际注入一致。
2.3 Hard Constraints:九条硬约束
这是整份提示词最核心的部分,原文档列出 9 条硬约束,逐条如下:
| # | 硬约束 | 设计意图 |
|---|---|---|
| 1 | 任何数据变更都必须调用 propose_resume_patches,禁止在聊天文本中直接输出原始 patch 数组 |
保证变更走工具通道,可被 UI 结构化渲染为审批卡片 |
| 2 | 生成满足请求所需的最小操作集合 | 防止模型“顺手重写”无关字段,缩小 diff 与误伤面 |
| 3 | 除非用户明确要求替换或删除,否则保留现有数据 | 默认保守编辑 |
| 4 | 破坏性编辑(删除、清空、替换大段内容)前必须先请求确认 | 人工审批兜底 |
| 5 | 保持简历主题聚焦,拒绝跑题请求 | 限制助手职责范围 |
| 6 | 不得虚构用户的事实履历;起草内容必须标注为草稿并请求确认 | 防幻觉污染简历这一事实性文档 |
| 7 | 所有 path 与 op 必须符合 RFC 6902 及当前 schema | 与后文 Zod 校验、fast-json-patch 校验双层防线呼应 |
| 8 | 新建条目的 ID 必须是 xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx 格式的 UUID |
与简历数据模型中条目 id 字段约定一致 |
| 9 | HTML 字段(如 summary/description)必须使用合法 HTML(按需使用 <p>、<ul>、<li>、<strong>、<em>) |
简历渲染层按 HTML 渲染富文本,纯文本会破坏排版 |
2.4 Conflict Resolution Order:冲突消解顺序
当指令之间存在张力时,原文档给出三级优先级:
- 数据安全与 schema 合法性(Data safety and schema validity)
- 来自最新指令的用户意图(User intent from latest instruction)
- 最小化编辑策略(Minimal-change editing strategy)
可以推断,这条顺序对应了实际系统的行为:即使用户意图与数据安全冲突,补丁也会被 patch.ts 的 applyResumePatches 拒绝(见第四节校验流程)——提示词是“软约束”,代码校验是“硬约束”,两者同向叠加。
2.5 Editing Rules:编辑规则
原文档给出五条具体操作规则:
- 优先使用定向的
replace操作,而非整对象替换; - 追加列表项时用
add指向/items/-(JSON Pointer 的数组末尾追加语法); remove仅在用户明确要求或已确认时使用;website对象保持{ "url": string, "label": string }的形状;hidden字段必须保持显式布尔值。
这些规则都是针对简历数据模型的“易错点”量身定制的。例如 add at /items/- 的用法在测试中有直接印证:patch.test.ts 中向 /sections/skills/items/- 追加一条技能,随后 result.sections.skills.items 长度变为 1,验证了该路径约定真实可用。
2.6 Resume Shape Reference:数据结构参考
原文档给模型提供了简历数据的“地图”,避免模型猜路径:
- 顶层键:
basics、summary、picture、sections、customSections、metadata; sections内的条目家族:profiles、experience、education、projects、skills、languages、interests、awards、certifications、publications、volunteer、references。
这份参考与 packages/schema 中定义的 ResumeData 结构对应。由于当前简历 JSON 本身会注入到提示词末尾(见 2.8 节),模型实际上有“地图 + 实景”双重参照,可以在生成 patch 前核对目标路径是否存在。
2.7 Output Contract 与 Proposal Shape:输出契约与提案形状
原文档对“怎么回话”和“提案长什么样”分别做了规定:
Output Contract(输出契约)
- 需要变更时:调用
propose_resume_patches,且不要再追加文本回复——提案细节由 UI 展示; - 无需变更时:不调用工具,直接给出简洁指导;
- 永远不要在聊天回复中包含 patch 负载的 Markdown 代码块。
Proposal Shape(提案形状)
- 以
proposals数组返回一个或多个内聚提案; - 每个提案包含
title、可选summary和operations; - 必须一起批准的操作归入同一提案;
- 不相关的变更拆成独立提案,方便用户逐页审批。
这两组约定与工具层 Schema 完全对齐:patch-proposal.ts 中 resumePatchProposalToolInputSchema 要求 proposals 至少一项,每项含必填 title、可选 summary 和至少一条 operations(z.array(jsonPatchOperationSchema).min(1))。patch-proposal.test.ts 明确断言“空 operations 的提案会被拒绝”。
2.8 动态注入当前简历数据
模板末尾保留了数据注入锚点:
## Current Resume Data
```json
{{RESUME_DATA}}
(上面为模板结构示意,实际占位符内容见 [chat-system.md](https://gitcode.com/GitHub_Trending/re/reactive-resume/blob/3fa9de140c926b4551ef95f83d8d7505cc16d9a4/packages/ai/src/prompts/chat-system.md?utm_source=gitcode_repo_files#L58-L62))
服务端在 [service.ts](https://gitcode.com/GitHub_Trending/re/reactive-resume/blob/3fa9de140c926b4551ef95f83d8d7505cc16d9a4/packages/api/src/features/ai/service.ts?utm_source=gitcode_repo_files#L464-L466) 中完成替换:
```ts
function buildChatSystemPrompt(resumeData: ResumeData): string {
return chatSystemPromptTemplate.replace("{{RESUME_DATA}}", JSON.stringify(resumeData, null, 2));
}
即每次对话请求时,当前简历的完整 JSON(2 空格缩进)都会被拼进系统提示词。这解释了 Hard Constraint 7“path 必须对当前 schema 有效”为何可行——模型确实看得见当前数据。同时注意注入的是未经裁剪的简历数据,意味着该接口只应在已鉴权、且用户拥有该简历的前提下调用(见下一节的 API 入口)。
三、API 入口:/ai/chat 与审批流
chat-system.md 描述的行为最终由 API 层落地。router.ts 中的 chat 过程:
- 使用
protectedProcedure,要求登录态; - 输入为
{ aiProviderId?, messages: UIMessage[], resumeId: string }; - 挂载
aiRequestRateLimit限流中间件; - handler 并行解析出“可运行的 AI Provider 凭据”和按
resumeId+ 当前用户 ID 查出的简历,然后调用aiService.chat,并把resume.updatedAt一并传入——它会成为每条提案的baseUpdatedAt,用于前端检测“提案基于哪个版本的简历”。
路由的描述文案本身就概括了这套机制:
Streams a chat response from the configured AI provider. The LLM can call the propose_resume_patches tool to generate JSON Patch proposals for explicit user approval.
注意“proposals for explicit user approval”——服务端不自动落库,用户在前端确认后才真正写入简历,这正是提示词 Objective 中“用户先审查”的代码侧兑现。
四、propose_resume_patches 工具的底层实现
4.1 工具注册与流式多步调用
在 service.ts 的 chat 函数中,模型通过 Vercel AI SDK 的 streamText 运行,工具在 tools 配置中注册:
tools: {
propose_resume_patches: tool({
description:
"Return one or more cohesive resume change proposals. Each proposal must include a title, optional summary, and valid JSON Patch operations against the current resume data. The tool validates but does not apply changes.",
inputSchema: resumePatchProposalToolInputSchema,
outputSchema: resumePatchProposalToolOutputSchema,
execute: (toolInput) => {
const proposals = normalizeResumePatchProposals(toolInput, input.resumeUpdatedAt);
for (const proposal of proposals) {
applyResumePatches(input.resumeData, proposal.operations);
}
return { proposals };
},
}),
},
stopWhen: stepCountIs(3),
两个实现细节值得注意:
- “validates but does not apply”:
execute中对每个提案调用applyResumePatches,而 patch.ts 的实现是“先校验、再在深拷贝上打补丁、最后对结果重跑parseResumeData校验”,不修改传入对象。因此这里的调用实质是一次“试算校验”:路径不存在、test断言失败或补丁后数据不合法都会抛错,让工具执行失败,从而把错误反馈回模型(stopWhen: stepCountIs(3)允许模型在最多 3 步内修正重试);校验通过才把提案原样返回给 UI 等待用户审批。 - 工具名与提示词严格一致:提示词中的
propose_resume_patches与注册键完全同名,[[RESUME_DATA]]注入、工具描述、提示词三者共同构成完整的工具契约。service.test.ts 中用type: "tool-propose_resume_patches"的消息断言了工具调用的流式输出形态。
4.2 提案的规范化:id 与 baseUpdatedAt
patch-proposal.ts 的 normalizeResumePatchProposals 做两件模型不必操心的事:
export function normalizeResumePatchProposals(input, baseUpdatedAt?) {
return input.proposals.map((proposal, index) => ({
...proposal,
id: proposal.id ?? `proposal-${index + 1}`,
...(baseUpdatedAt ? { baseUpdatedAt: baseUpdatedAt.toISOString() } : {}),
}));
}
- 模型可以不生成
id(输入 Schema 中id是 optional),服务端按顺序补proposal-1、proposal-2……,降低模型出错面; - 把简历的
updatedAt(Date)统一序列化为 ISO 字符串写入每条提案的baseUpdatedAt。
patch-proposal.test.ts 验证了这两点:无 id 输入可正常解析;baseUpdatedAt 被规范为 JSON 安全的 ISO 时间戳(测试中为 2026-05-10T06:38:27.093Z)。
4.3 JSON Patch 操作的结构化校验
提示词要求“op 与 path 符合 RFC 6902 及当前 schema”,代码侧由 patch.ts 的判别联合 Schema 把六种操作约束在请求边界上:
export const jsonPatchOperationSchema = z.discriminatedUnion("op", [
z.object({ op: z.literal("add"), path: z.string(), value: z.unknown() }),
z.object({ op: z.literal("remove"), path: z.string() }),
z.object({ op: z.literal("replace"), path: z.string(), value: z.unknown() }),
z.object({ op: z.literal("move"), path: z.string(), from: z.string() }),
z.object({ op: z.literal("copy"), path: z.string(), from: z.string() }),
z.object({ op: z.literal("test"), path: z.string(), value: z.unknown() }),
]);
[patch.test.ts](https://gitcode.com/GitHub_Trending/re/reactive-resume/blob/3fa9de140c926b4551ef95f83d8d7505cc16d9a4/packages/resume/src/patch.test.ts?utm_source=gitcode_repo_files#L6-L56) 逐一验证了字段约束:add 必须有 value、move 必须有 from、未知 op(如 swap)被拒绝——这意味着模型即使生成非法 op,也会在 AI SDK 的输入校验层(inputSchema)就被拦下。
4.4 补丁应用:校验、试算与回滚
applyResumePatches(patch.ts)的三步流程与提示词“Data safety and schema validity 优先”的冲突消解顺序一一对应:
- 结构预校验:
jsonpatch.validate(operations, data)失败即抛出ResumePatchError,携带code、index(失败操作下标)和operation(失败操作对象),但刻意不携带整棵文档树; - 试算应用:
jsonpatch.applyPatch(data, operations, false, false)在内部克隆的文档上执行,test断言不匹配会直接抛错; - 结果 schema 回滚校验:补丁后的文档必须通过
parseResumeData,否则抛出Patch produced invalid resume data——即使每个操作本身合法,只要整体结果不符合简历 schema 也整体拒绝。
对应的错误语义表(patch.ts)把 fast-json-patch 的内部错误码翻译为人类可读消息,例如 OPERATION_PATH_UNRESOLVABLE(路径不存在)、OPERATION_VALUE_OUT_OF_BOUNDS(数组越界)、TEST_OPERATION_FAILED(断言不匹配)。测试 patch.test.ts 展示了回滚的实际效果:把 /picture/size 替换为 9999(超出 schema 的 32–512 范围)会整体抛错,原数据保持不变(另有测试断言输入对象不被修改)。
4.5 端到端防线总结
把提示词与源码串联起来,一条“把 TypeScript 加进技能列表”的用户指令的完整链路是:
- 系统提示词(含当前简历 JSON)+ 对话历史发往模型;
- 模型按 Hard Constraints 生成最小操作,调用
propose_resume_patches,例如对/sections/skills/items/-执行add(提示词 Editing Rule 与 patch.test.ts 的用法一致); - AI SDK 用
resumePatchProposalToolInputSchema校验工具输入(title 非空、operations 至少一条); normalizeResumePatchProposals补id/baseUpdatedAt;applyResumePatches做“预校验 → 试算 → 结果 schema 校验”,任何一步失败都让工具执行报错,模型在 3 步内可修正;- 校验通过的提案流式返回前端,渲染为审批卡片;
- 用户批准后,变更才真正写入简历数据。
五、从这份提示词中可以提取的设计模式
以 chat-system.md 为主体回看,它示范了一套可复用的“LLM 修改结构化数据”提示词工程模式:
- 单一变更通道:把“怎么改数据”收敛到一个具名工具上,聊天文本只负责解释与确认(Output Contract 三条),使 UI 可确定性地解析并渲染每一次变更;
- 把数据模型地图写进提示词:顶层键、条目家族、
website/hidden等易错字段形状(2.5、2.6 节),配合运行期注入的完整 JSON,把“路径猜错”这类高频错误压到最低; - 约束与校验同构:提示词中的 RFC 6902 要求对应
jsonPatchOperationSchema;“不虚构履历、标注草稿”对应结果 schema 回滚校验;“破坏性操作先确认”对应用户审批环节。提示词负责“让模型倾向正确”,代码负责“让错误无法生效”; - 提案分组策略:“必须一起批准的放一组,不相关的拆开”(Proposal Shape)让 diff 粒度匹配用户的心智审批粒度,而不是让一次对话产生一个无法整体否决的大补丁;
- 模板与代码解耦:提示词以独立 Markdown 文件存放、启动时读取、占位符运行期替换(prompts.ts),使文案迭代不触碰业务逻辑,且可被 prompts.test.ts 这类轻量断言守护。
如果你在为结构化数据(简历、配置、文档树)构建 AI 编辑助手,这套“模板提示词 + 工具 Schema 双端对齐 + 试算式校验 + 人工审批”的组合,可以直接作为设计参照;而本文涉及的所有关键实现,都可以从 packages/ai/src/prompts/chat-system.md、packages/api/src/features/ai/service.ts、packages/ai/src/tools/patch-proposal.ts 与 packages/resume/src/patch.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 StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00