系列文章之二。本文深入讲解项目的核心创新——RAG 事实约束引擎,解决 LLM 简历优化中"编造事实"的根本问题。
0. 问题:LLM 简历优化的"幻觉"困境
用 LLM 优化简历,最大的痛点不是"写不好",而是"编太多"。
你让 LLM 优化一段简历描述,它可能会这样改:
原文:
负责后端接口开发,使用 Java 和 Spring Boot,优化了部分接口性能
LLM 优化后(常见幻觉):
主导后端核心接口架构设计与开发,基于 Java + Spring Boot 微服务框架,通过引入缓存机制和 SQL 优化,使接口响应时间降低 50%,QPS 提升 3 倍
看到了吗?“主导”、“架构设计”、“缓存机制”、“SQL 优化”、“降低 50%”、“QPS 提升 3 倍”——原文一个都没有。
这在面试中是致命的。面试官问:“你说的缓存机制是什么?SQL 优化具体怎么做的?” 答不上来,直接挂。
传统方案是用 Prompt 约束:“不要编造数据”、“只优化表达不改变事实”。但 Prompt 约束不可靠,LLM 经常"理解偏差",该约束的地方不约束,不该约束的地方反而过度约束。
我的方案:不依赖 Prompt,用代码级别的硬约束——提取原文事实,生成后校验,分级处理。
1. 事实约束引擎设计
1.1 整体思路
用户原文 ──→ 提取事实 ──→ LLM 优化 ──→ 校验生成 ──→ 分级处理 ──→ 最终输出 │ │ │ │ ▼ ▼ ▼ ▼ 数字集合 优化文本 新数字集合 硬约束:删除 技术词集合 事实报告 新技术词集合 软约束:标记 中文关键词 校验结果 对比结果 白名单:允许增强核心思想:事实是锚点,优化是装饰。锚点不能动,装饰可以加。
1.2 事实提取
importrefromtypingimportDict,Set,Tuple# 技术栈白名单(常见的编程技术术语)TECH_KEYWORDS={"java","spring","spring boot","spring cloud","mybatis","mysql","redis","mongodb","elasticsearch","kafka","rabbitmq","docker","kubernetes","k8s","nginx","linux","git","maven","gradle","python","django","flask","fastapi","pandas","numpy","torch","tensorflow","langchain","langgraph","llm","rag","向量"," embeddings","react","vue","angular","typescript","javascript","html","css","go","golang","grpc","protobuf","grpc","protobuf","aws","azure","gcp","aliyun","tencent cloud","k8s","docker","jenkins","github actions","gitlab ci","prometheus","grafana","logstash","kibana","filebeat","tcp","udp","http","https","websocket","rest","graphql","sql","nosql","orm","migrations","seed","微服务","分布式","高并发","缓存","消息队列","负载均衡","幂等","熔断","限流","降级","灰度","A/B",}# 允许增强的动词白名单(可以升级表达,但不改变事实)ENHANCEMENT_VERBS={"负责","主导","参与","设计","开发","实现","优化","重构","搭建","部署","维护","监控","分析","调研","评估","规划","协调","推进","落地","落地","落地","落地",}defextract_facts(text:str)->Dict[str,Set[str]]:"""从原始文本中提取可验证的事实锚点"""# 1. 提取所有数字(整数和小数)numbers=set(re.findall(r'\d+\.?\d*',text))# 2. 提取技术关键词(大小写不敏感)text_lower=text.lower()tech_terms:Set[str]=set()forkwinTECH_KEYWORDS:ifkw.lower()intext_lower:tech_terms.add(kw)# 3. 提取中文核心关键词(2-4字的中文词组)cn_keywords=set(re.findall(r'[\u4e00-\u9fff]{2,4}',text))return{"numbers":numbers,"tech_terms":tech_terms,"cn_keywords":cn_keywords,}提取逻辑说明:
- 数字:用正则
\d+\.?\d*提取所有数字,包括整数和小数。这些是硬约束锚点——原文有的数字可以保留或增强(比如"优化了接口"→"接口响应时间降低 30%"),但原文没有的数字绝对不能出现。 - 技术词:维护一个技术栈白名单,检查原文中出现了哪些。优化后如果出现了白名单外的技术词,标记为可疑。
- 中文关键词:提取 2-4 字的中文词组作为语义锚点,帮助判断优化是否偏离了原文语义。
2. 校验与分级处理
2.1 校验逻辑
defvalidate_generation(generated:str,original_facts:Dict[str,Set[str]],)->Dict[str,any]:"""校验生成内容是否编造了新事实"""# 提取生成内容中的新数字new_numbers=set(re.findall(r'\d+\.?\d*',generated))hallucinated_numbers=new_numbers-original_facts["numbers"]# 提取生成内容中的新技术词generated_lower=generated.lower()new_tech=set()forkwinTECH_KEYWORDS:ifkw.lower()ingenerated_lowerandkwnotinoriginal_facts["tech_terms"]:new_tech.add(kw)# 提取生成内容中的新中文关键词new_cn=set(re.findall(r'[\u4e00-\u9fff]{2,4}',generated))suspect_cn=new_cn-original_facts["cn_keywords"]return{"hallucinated_numbers":list(hallucinated_numbers),# 硬约束:新增数字"suspect_tech_terms":list(new_tech),# 软约束:新增技术词"suspect_keywords":list(suspect_cn),# 软约束:新增中文词"is_safe":len(hallucinated_numbers)==0,# 是否安全"risk_level":classify_risk(hallucinated_numbers,new_tech),# 风险等级}defclassify_risk(hallucinated_numbers:list,new_tech_terms:list,)->str:"""根据幻觉程度分类风险等级"""ifhallucinated_numbers:return"high"# 高风险:编造了数字ifnew_tech_terms:return"medium"# 中风险:编造了技术词return"low"# 低风险:安全2.2 分级处理策略
defsanitize_generation(generated:str,original_facts:Dict[str,Set[str]],validation:Dict[str,any],)->Dict[str,str]:"""根据校验结果分级处理生成内容"""result=generated# === 硬约束:过滤编造的数字 ===ifvalidation["hallucinated_numbers"]:fornuminvalidation["hallucinated_numbers"]:# 删除原文没有的数字(保留有原文数字的上下文)result=re.sub(rf'\b{re.escape(num)}\b','',result)# 清理多余空格result=re.sub(r'\s{2,}',' ',result).strip()# === 软约束:标记可疑技术词 ===suspect_terms=validation["suspect_tech_terms"]ifsuspect_terms:forterminsuspect_terms:# 用特殊标记包裹可疑技术词,方便前端展示提醒result=result.replace(term,f'【{term}?原文未提及,请确认】')# === 白名单增强:允许动词升级 ===# 这部分不修改内容,只是在 Prompt 中告诉 LLM 可以使用这些动词# 例如:"负责" → "主导","参与" → "深度参与","优化" → "深度优化"return{"sanitized_text":result,"has_hallucinations":len(validation["hallucinated_numbers"])>0,"has_suspect_terms":len(suspect_terms)>0,"risk_level":validation["risk_level"],}分级处理策略总结:
| 级别 | 触发条件 | 处理方式 | 示例 |
|---|---|---|---|
| 硬约束 | 新增了原文没有的数字 | 直接删除该数字 | 原文"优化了接口",生成"降低50%" → 删除"50%" |
| 软约束 | 新增了原文没有的技术词 | 标记为可疑,保留内容 | 原文没提"Redis",生成中出现了 → 标记"【Redis?原文未提及,请确认】" |
| 白名单增强 | 使用白名单动词 | 允许措辞升级 | “负责” → “主导”,“参与” → “深度参与” |
3. RAG 知识库注入
3.1 知识库结构
项目内置了一个 STAR 案例库(data/kb/star_cases.json),包含 12 个高质量范例,覆盖算法、后端、前端、数据、产品、运营等方向。
每个案例包含:
{"id":"backend-java-001","field":"backend","title":"高并发订单系统重构","background":"电商平台订单系统日均处理 10 万+ 订单...","architecture":"采用 Spring Cloud 微服务架构...","responsibility":"负责订单模块的核心开发...","results":"接口响应时间从 500ms 降低到 50ms...","tech_stack":"Java, Spring Boot, MySQL, Redis, Kafka...","star_example":"S: 电商平台订单系统日均处理 10 万+ 订单...","quantification_phrases":["提升了...","降低了...","缩短了...","增加了..."]}3.2 BM25 检索
fromtypingimportList,TupleclassBM25Retriever:"""基于 BM25 的本地检索器"""def__init__(self,documents:List[str]):self.documents=documents self.idf=self._compute_idf()def_compute_idf(self)->Dict[str,float]:"""计算逆文档频率"""doc_freq={}fordocinself.documents:words=set(doc.lower().split())forwordinwords:doc_freq[word]=doc_freq.get(word,0)+1n=len(self.documents)return{word:math.log((n-count+0.5)/(count+0.5)+1)forword,countindoc_freq.items()}defsearch(self,query:str,top_k:int=3)->List[Tuple[str,float]]:"""检索最相关的文档"""query_words=set(query.lower().split())scores=[]fori,docinenumerate(self.documents):doc_words=set(doc.lower().split())# BM25 简化版:基于词频和 IDF 的加权评分score=0forwordinquery_words:ifwordindoc_words:tf=doc.lower().count(word)idf=self.idf.get(word,0)score+=tf*idfifscore>0:scores.append((doc,score))# 按评分排序,返回 top_kscores.sort(key=lambdax:x[1],reverse=True)returnscores[:top_k]3.3 注入到 Prompt
在简历优化的 Prompt 中,检索到的 STAR 案例会被注入到 System Prompt:
defbuild_optimize_prompt(resume_text:str,original_facts:Dict[str,Set[str]],retrieved_cases:List[Tuple[str,float]],)->str:"""构建优化 Prompt,注入 RAG 检索结果"""# 拼接检索到的 STAR 案例rag_context=""forcase_text,scoreinretrieved_cases:rag_context+=f"【参考案例】{case_text}\n"prompt=f""" 你是一个专业的简历优化专家。请根据以下规则优化用户的简历: ## 事实约束(硬性要求) - 原文中的数字(如"10万+用户"、"30%提升")**必须保留** - 原文中没有的数字**绝对不能出现** - 原文中的技术栈关键词**必须保留** - 原文中没有的技术词**不能随意添加** ## 优化规则 - 使用 STAR 法则重构项目描述:背景 → 架构 → 负责 → 成果 → 技术栈 - 允许使用白名单动词升级表达:"负责"→"主导"、"参与"→"深度参与" - 量化成果时只能使用原文中的数字或合理推算 ## 参考案例{rag_context}## 用户原始简历{resume_text}请输出优化后的简历内容(Markdown 格式)。 """returnprompt4. 效果对比
4.1 优化前(无事实约束)
原文:
负责后端接口开发,使用 Java 和 Spring Boot,优化了部分接口性能
LLM 优化(无约束):
主导后端核心接口架构设计与开发,基于 Java + Spring Boot 微服务框架,通过引入 Redis 缓存机制和 SQL 查询优化,使接口响应时间降低 50%,QPS 从 100 提升到 500
问题:“主导”、“架构设计”、“Redis 缓存”、“SQL 优化”、“降低 50%”、“QPS 提升 5 倍”——全部编造。
4.2 优化后(有事实约束)
原文:
负责后端接口开发,使用 Java 和 Spring Boot,优化了部分接口性能
LLM 优化(有约束):
负责后端接口开发,使用 Java 和 Spring Boot,优化了部分接口性能,提升了系统稳定性
事实校验:
- 数字:原文无数字,生成无新增数字 ✅
- 技术词:Java、Spring Boot 原文有 ✅
- 动词:“负责” 原文有,未升级为"主导" ✅
- 新增内容:“提升了系统稳定性”——软约束标记为可疑,但因为是通用描述,保留
面试应对:面试官问"优化了什么接口?怎么优化的?" 用户可以基于原文如实回答,不会被编造的内容坑死。
5. 工程实现细节
5.1 节点集成
事实约束引擎集成在 LangGraph 流水线的verify_facts节点中:
asyncdefverify_facts_node(state:ResumeOptimizeState)->ResumeOptimizeState:"""事实校验节点:过滤编造内容,标记可疑信息"""original_text=state["original_resume"]optimized_text=state["optimized_items"]# 1. 提取原文事实original_facts=extract_facts(original_text)# 2. 对每个优化项进行校验sanitized_items=[]foriteminoptimized_text:validation=validate_generation(item["optimized"],original_facts)result=sanitize_generation(item["optimized"],original_facts,validation)# 3. 附加校验报告item["validation_report"]={"risk_level":result["risk_level"],"has_hallucinations":result["has_hallucinations"],"has_suspect_terms":result["has_suspect_terms"],}item["optimized"]=result["sanitized_text"]sanitized_items.append(item)return{**state,"optimized_items":sanitized_items,"fact_check_passed":all(notitem["validation_report"]["has_hallucinations"]foriteminsanitized_items),}5.2 前端展示
校验报告会传递给前端,在优化结果中展示风险等级:
// 前端渲染优化结果<div v-for="item in optimizedItems":key="item.id"><divclass="risk-badge":class="item.validation_report.risk_level">{{{low:'安全',medium:'需确认',high:'有风险'}[item.validation_report.risk_level]}}</div><divclass="optimized-text"v-html="item.optimized"></div><div v-if="item.validation_report.has_suspect_terms"class="suspect-terms">以下技术词原文未提及,请确认:{{item.validation_report.suspect_terms.join('、')}}</div></div>6. 总结
RAG 事实约束引擎的核心价值:
- 硬约束过滤:用代码级别的数字校验,杜绝 LLM 编造数据
- 软约束标记:对可疑内容做标记提醒,不直接删除(避免过度约束)
- 白名单增强:允许合理的措辞升级,提升表达质量
- RAG 注入:用真实案例引导 LLM 学习 STAR 写法,而不是凭空想象
这个机制使得优化后的简历忠实于用户原始素材,同时显著提升表达质量。用户反馈:“优化后仍像自己写的,但更专业了。”
*作者:达不溜记 | 项目地址:https://gitee.com/qinghe-sixteen/multiagentmianshizhu
系列文章预告:
- 系列三:LangGraph 简历优化流水线实战(8 节点详解)
- 系列四:全栈开发实战(FastAPI + Vue3 前后端联调)
- 系列五:踩坑记录与工程实践