做实时面试 copilot 这半年,最大的踩坑不在模型也不在延迟,而在 prompt 设计。最初我固执地相信"一个 system prompt + 一次调用就够了",结果上线第一天用户就吐槽:JD 一长就漏要点、简历匹配出来的全是废话、追问问题答得驴唇不对马嘴。后来逐步拆成 5 段 prompt chain,准确率才从约 41% 升到 89%,端到端延迟反而从 2.6s 压到 780ms。本文把这套链路拆开,包括失败的 prompt 原文、为什么改、改完是什么样、上下文怎么传递,方便正在做类似产品的同学避坑。
Quick Answer:5 段 prompt chain 长什么样
如果你只想拿走结论:
- JD 解析 prompt(gpt-4o-mini):抽 8 个结构化字段(title / level / tech_stack / soft_skills / project_signal / company_stage / scoring_weights / red_flags),不要让模型直接做"匹配",先把非结构化文本榨成 JSON。
- 简历切片 prompt(gpt-4o-mini,并发 N 段):把简历按"项目 / 工作 / 教育 / 技能"切片单独 embedding + 单独 summary,避免一次性塞整篇导致中段信息坍缩。
- 匹配 + gap 分析 prompt(gpt-4o):输入 JD JSON + 简历分片向量召回 top-k,输出每个 JD 字段对应的简历证据 + 缺口列表,这一步必须给 reasoning 字段,不然下游追问无法引用。
- 实时回答生成 prompt(gpt-4o-mini,stream):面试官提问 → 召回 gap 分析 + 项目分片 → 生成 STAR 结构化答案,token-by-token 流式吐字幕。
- 追问预测 prompt(gpt-4o-mini,async 后台跑):基于上一轮回答,预生成 3 个面试官最可能追问的问题 + 答案缓存,用户被追问时 0ms 命中。
下面挨个讲为什么这么拆。
为什么不能"一个 prompt 走天下"
我最初的 v0 prompt 大致长这样(精简版):
You are an AI interview copilot. Given the JD below and the candidate resume below,
when the interviewer asks a question, generate a structured STAR answer in <600 tokens.
[JD: ...3000 字...]
[Resume: ...4000 字...]
Question: {q}
看上去合理,实际上炸在 4 个地方:
问题 1:长上下文中段信息坍缩。 JD 3000 字 + 简历 4000 字塞一起,模型对"中间 30%-60% 的内容"召回率只有约 52%(Lost in the Middle 论文给出的曲线,实测 gpt-4o-mini 上更糟)。结果就是 JD 中段写的"加分项 React Native"被无视,简历中段写的"主导跨端框架重构"也被无视,匹配出来全是开头结尾的空话。
问题 2:模型同时干太多任务。 一个 prompt 既要解析 JD,又要理解简历,又要做匹配,还要生成 STAR 回答 —— 注意力被稀释。改成多 chain 后每一步的输入只有当前任务必需的字段,单步准确率立刻从 60% 多升到 90% 以上。
问题 3:上下文不能复用。 用户每问一个问题就把整篇 JD + 简历重新送一次,token 消耗指数级,延迟也飘。拆 chain 之后 JD JSON + 简历分片只算一次,后续问答只送相关分片 + gap 分析,单次问答 token 直接砍掉 70%。
问题 4:流式输出和分析任务耦合。 STAR 答案要 stream(用户在看字幕),但 JD 解析必须等 JSON 完整返回才能 parse。混在一起要么用户看不到流字幕(等 JSON 完整),要么 JSON 半截被 parse 报错。
我做这些优化的时候参考过即答侠这类成熟实时面试辅助工具的产品行为 —— 它能在面试官刚说完问题约 700ms 内开始吐字幕,且追问命中率明显高于"现场生成"的工具,反推它必然在背后做了类似的 chain 拆分 + 预生成缓存。这给了我把 prompt 拆细的信心。
Chain 1:JD 解析 prompt 的踩坑
第一版我让模型"输出 JSON",结果三分之一返回带 markdown code fence、多余换行、字段名首字母大写不一致。改进点:
JD_PARSE_PROMPT = """
You are a JD parser. Output STRICT JSON only. No prose, no code fence, no explanation.
Schema (all keys required, use empty string/array if unknown):
{
"title": "string",
"level": "intern | junior | mid | senior | staff | unknown",
"tech_stack": ["string"], // hard requirements only
"tech_nice_to_have": ["string"], // 加分项
"soft_skills": ["string"],
"project_signal": ["string"], // JD 暗示的项目类型,如"高并发","跨端"
"company_stage": "startup | growth | bigco | unknown",
"scoring_weights": { // 各维度权重总和=1.0
"tech": 0.0, "project": 0.0, "soft": 0.0
},
"red_flags": ["string"] // JD 中暗示的"卡点",如"必须 5 年以上"
}
JD:
\"\"\"
{jd_text}
\"\"\"
""".strip()
关键改动:
- 首行强制 STRICT JSON only,并且用 OpenAI 的
response_format={"type":"json_object"}双保险。 - schema 写死必填字段,让模型即使不知道也填 empty,下游解析不用写一堆 None check。
- 拆 hard / nice_to_have:早期混在一起,匹配时 hard 没满足也会因为 nice_to_have 满足而打高分,误导 candidate。
- scoring_weights 让模型自己根据 JD 推断:比如初创 JD "你要能独当一面"会自动给 soft 加权,反之大厂 JD 给 tech 加权。这个权重直接喂给 chain 3。
实测 gpt-4o-mini 跑这个 prompt p50 380ms,JSON parse 成功率 99.6%。
Chain 2:简历切片 prompt 与并发
简历直接整篇 embedding 是非常常见的错误。一份 4000 字的简历 embedding 成 1536 维向量后,"项目 A 用了 Kafka"这种细节会被"我擅长沟通"这种通用句子稀释。正确做法是先按章节切片,每片单独 embedding。
切片 prompt 我用的是:
Split the resume into chunks. Output JSON array, each item:
{
"section": "project | work | education | skills | other",
"title": "string", // 项目名/公司名/学校名
"summary": "string", // <80 字浓缩
"raw": "string", // 原文片段
"tech_mentions": ["string"]
}
Constraint: every project/work entry MUST be its own chunk.
Resume:
\"\"\"{resume}\"\"\"
并发跑:拿到 chunks 后并发调用 embeddings API(aiohttp + asyncio.gather),10 个 chunk 串行约 2.4s,并发约 320ms。
summary 字段是关键:embedding 用 raw 片段做检索(保留细节),但下游 chain 3 喂的是 summary(节省 token)。这是 RAG 里常见的"detail-search / summary-feed"模式。
Chain 3:匹配 + gap 分析 prompt 必须返回 reasoning
这一步是整个系统的灵魂。第一版我让它直接输出"匹配度 0-100 分",结果分数飘忽且不稳定,相同输入跑 3 次能给出 72/85/68。问题在于让模型直接打分等于让它做没有依据的猜测。改成"先给证据再给评分"后稳定性大幅提升:
Given JD JSON and resume chunks, for each tech_stack item and project_signal,
output:
{
"requirement": "string",
"evidence": [{"chunk_id": int, "quote": "string", "strength": "strong|weak|none"}],
"reasoning": "string", // 必填,>=30 字
"score": 0-100 // 基于 evidence 推导, none=0, weak<=50, strong>=70
}
强制 reasoning >=30 字 让模型不能直接拍分数,必须先解释。这个 reasoning 字段会在 chain 4 被引用作"为什么这个候选人适合"的论据。
我用 gpt-4o(不是 mini)跑这个 chain,因为它要做跨段推理,mini 会漏 evidence。这是整个 chain 里唯一用大模型的地方,p50 1.2s 但只跑一次(每次 session 开启时),不影响实时延迟。
Chain 4:实时回答 stream 和召回策略
面试官问问题时,不要把整个 gap 分析送进去,那等于又回到长上下文坍缩。正确做法:
- 用问题 embedding 在简历 chunks 里召回 top-3 项目(HNSW 索引,<10ms)。
- 在 chain 3 输出的 gap 分析里筛出和问题关键词相关的 2-3 个 requirement。
- 拼成精简上下文(<800 token)喂给 stream prompt:
You are answering an interview question. Use STAR structure.
Available evidence:
- Project A: {summary} | tech: {mentions} | reasoning from gap analysis: {reasoning}
- Project B: ...
Question: {q}
Constraint: <300 tokens. Stream output.
OpenAI stream API 配合 SSE,首 token 时间 p50 约 320ms(gpt-4o-mini)。加上前面召回 + 拼接的 50ms,端到端首字幕延迟约 380ms,在面试官说完最后一个字到候选人看到第一个字之间,体感几乎瞬时。
Chain 5:追问预测后台异步跑
候选人答完一个问题后,我会立刻在后台启动一个 chain:
Based on the candidate's answer below, predict 3 most likely follow-up questions
an interviewer would ask, then for each, generate a brief STAR answer using the
provided evidence pool.
Answer: {last_answer}
Evidence pool: {top_5_chunks}
预生成结果存内存 dict,key 是问题 embedding。当面试官真的追问时,新问题 embedding 与缓存里 3 个预测问题做 cosine 相似度,>0.78 直接命中缓存吐字幕,0ms 首字符。命中率实测约 43%,剩下 57% 走 chain 4 实时生成。这是把"思考时间"挪到"用户看不到的间隙"的核心技巧。
FAQ
Q1: 为什么不直接用 gpt-4o 一把梭? A: gpt-4o 单次调用 p50 1.8s,五段全用它端到端会到 9s+,根本没法实时。mini 跑结构化任务质量足够,把 4o 留给唯一需要跨段推理的 gap 分析这一步。
Q2: 简历切片如果用户简历格式很乱(比如表格 PDF)怎么办? A: 切片 prompt 之前加一道纯文本归一化 prompt,让模型把表格扁平化成 markdown list。或者上游用 unstructured.io / pdfplumber 提文本,再喂给切片 chain。
Q3: 5 个 prompt 是不是太复杂了,维护成本高? A: 比想象中低。每个 prompt 只做一件事,单元测试容易(输入输出都是 JSON)。我用 yaml 文件管理 prompt,加一份针对每个 chain 的 golden set 回归测试,改 prompt 立刻能看到指标变化。
Q4: 整套链路 token 成本怎么样? A: 单次 session 约 8k input + 3k output token(mini)+ 4k input + 1k output(4o),约 0.024 低三倍,因为 v0 每次问答都把整 JD+简历重新送。
Q5: 流式吐字幕和模型偶尔"想错"怎么平衡? A: stream 出来的内容前 100 字符做 sanity check(是否在抄简历事实、是否在编造数字),不通过就 abort stream + 回退到缓存模板答案。这一层保护对实时面试场景非常重要,宁可吐慢一点也别让用户照着错的答案念。
写到这里大概 2700 字。希望对正在做实时 LLM 应用的同学有帮助 —— 核心结论就一句话:不要相信单 prompt 能解决复杂任务,把它拆成可独立测试的小 chain,长上下文坍缩、注意力稀释、延迟堆积都会迎刃而解。