让AI标书生成器拒绝编造:基于事实库与RAG的工程化方案 在招投标文档自动生成项目里用大模型写标书有一个绕不开的痛点模型会把不存在的项目业绩、资质证书、人员履历写得“有鼻子有眼”。看似节省了人力实际上埋下了废标、资质造假、法律纠纷等隐患。本文围绕 “Making an AI bid writer refuse to lie” 这个目标梳理一套完整的工程化方案从提示词约束、事实库检索、结构化输出到后置校验一步步构建一个“宁肯说不知道也不编造标书内容”的 AI 投标书生成器。无论是想优化内部标书工具还是准备做 AI 应用落地这套思路都值得参考。1. 背景为什么 AI 投标书生成器会“说谎”1.1 大模型幻觉与投标场景的矛盾大模型生成文本时本质是在预测下一段最“自然”的内容而不是查询数据库。它并不知道自己的回答是否真实存在因此在缺少足够上下文时很容易产生“幻觉”。在普通问答场景中幻觉可能只是无伤大雅的错误但在投标书生成场景中AI 一旦编造出“某年某月中标某项目”“某产品通过某认证”后果会非常严重。一份投标文件通常包含商务标、技术标、价格标等多部分其中大量内容都需要与企业真实资质、历史业绩、人员证书严格对应。如果 AI 凭空生成这些内容项目组反复核对后可能仍会遗漏进而导致废标甚至被认定为提供虚假材料。这与“提升效率”的初衷完全背离。1.2 目标定义拒绝说谎不是拒绝生成“拒绝说谎”并不是让 AI 闭嘴不写而是让系统在事实不足时明确表达“未找到依据”“需要人工补充”并保证所有生成内容都能溯源到可信知识库。这个目标可以拆成三条原则事实来源唯一所有企业名称、项目金额、证书编号、人员信息必须来自企业授权导入的资料库不允许模型凭空补全。未见不说检索不到相关内容时模型必须输出缺失描述而不是硬凑一段“模板化但无依据”的文字。人工闭环AI 生成的承诺性、数据性内容必须经过人工复核和审批再进入正式标书。这三条原则是后文代码实现的逻辑基础。不要把希望完全寄托在“提示词让它别撒谎”上系统级约束远比一句 Prompt 可靠得多。1.3 典型应用场景这种“基于事实库的受限生成”能力可以用在很多环节商务标编写根据历史业绩表自动生成“近年类似项目案例”章节。技术方案生成根据企业产品白皮书、技术架构文档生成方案概述禁止编造指标。资质响应根据证书库生成“资格证明文件说明”避免错写证书有效期。偏离表与应答针对招标文件的每一条技术参数基于产品参数库做出“满足/不满足/偏离”判断。事实上标书只是一个缩影。凡是需要“有据可依”的合同、报告、合规文书都可以复用这套设计。2. 总体架构与核心思路2.1 一个最小可用的 Bid Writer 系统组成要让 AI 投标书生成器“拒绝说谎”不能只写一段 Prompt而应该把它当作一个典型的 RAG 应用来设计。最小闭环系统包含五个模块模块职责关键产出资料库存放企业真实资质、业绩、证书、方案等结构化/非结构化数据带唯一编号的事实记录检索模块根据用户问题召回最相关的资料片段事实列表和来源编号生成模块基于召回内容生成标书段落限定条件下的草稿校验模块检查输出内容是否超出事实边界校验通过/需人工复核人工审核台对未确定信息进行确认或修改最终可用标书内容在这套架构中“生成模型”只是中间的一环。真正决定 AI 是否说谎的不是模型本身而是围绕它的知识边界和校验规则。2.2 “拒绝说谎”的关键控制点从用户输入到最终输出有四个控制点需要重点设计输入侧明确告诉模型“你只能使用资料库中检索到的信息不要参考内部知识”。检索侧检索结果必须包含充分的来源信息如果为空直接转入“缺失处理”分支。输出侧使用结构化输出格式要求模型返回引用依据列表和缺失项清单。后校验侧用规则和实体匹配检查生成文本中是否存在“来历不明”的数字、公司名、证书号。这四个控制点可以叠加使用。哪怕某一层被绕过后一层仍然能拦截大部分问题。2.3 技术选型说明本文的示例代码使用 Python 编写核心依赖包括openai用于调用大模型接口pandas或csv用于管理事实资料表检索模块为了便于演示采用中文分词和关键词打分实现实际上可替换为向量数据库或 Elasticsearch。需要特别说明的是不同项目的大模型版本、接口参数差异很大示例代码中的模型名称、API 地址需要根据你自己的环境调整。文章重点强调的是工程链路而不是某个具体版本。3. 环境准备与工程目录3.1 运行环境本文示例在普通开发机上即可运行。建议环境如下操作系统Windows / macOS / Linux 均可Python 版本3.9 或以上大模型 API需要你有可用的 OpenAI API Key或兼容 OpenAI 协议的服务地址额外依赖pandas、openai如果使用分词需要jieba。如果你不想引入太多依赖也可以把jieba换成最简单的中文分词方式例如按字切分或用逗号、空格切分但效果会下降。代码示例会标注哪些是可选优化项。3.2 创建项目结构建议按照下面的目录结构组织代码bid_writer/ ├── data/ │ └── facts.csv ├── src/ │ ├── __init__.py │ ├── finder.py │ ├── generator.py │ ├── validator.py │ └── main.py ├── output/ │ └── generated_bid.md └── requirements.txt这样的结构清晰地把数据、检索、生成、校验分离开。即使后续要增加向量检索或权限模块也不需要改动主流程太多。3.3 准备可信事实库事实库是本项目最重要的资产。我们可以先准备一份最小化的facts.csv用来演示完整的链路。示例文件内容如下id,category,content F001,资质,公司持有ISO9001质量管理体系认证证书证书编号QMS-2024-001。 F002,资质,公司持有信息安全服务资质认证证书CCRC三级。 F003,业绩,2023年完成xx市智慧园区平台建设项目合同金额280万元项目顺利验收。 F004,业绩,2022年完成某大型制造企业MES系统实施项目合同金额450万元。 F005,人员,项目负责人张三持有PMP项目管理专业认证。 F006,方案,xx智慧园区平台采用微服务架构支持设备接入、数据分析和可视化大屏。在真实项目中这张表会复杂很多每条记录应该增加负责人、更新时间、原始文件地址、有效期等字段。但为了把原理讲清楚这里只保留最核心的列。需要提醒的是事实库中的数据必须经过企业授权并需要定期复核。不能把公开渠道抓来的企业信息直接当作内部事实否则依然存在误导风险。4. 核心代码实现4.1 事实检索模块检索模块的目标是根据用户问题从facts.csv中找到最相关的若干条记录。我们采用“关键词命中”的方式来实现思路简单且容易理解。实际项目中可以替换为向量检索但检索结果的结构仍然保持一致。创建src/finder.py文件内容如下# 文件路径src/finder.py import csv from typing import List, Dict try: import jieba USE_JIEBA True except ImportError: USE_JIEBA False class FactFinder: def __init__(self, csv_path: str): self.facts: List[Dict[str, str]] [] with open(csv_path, r, encodingutf-8) as f: reader csv.DictReader(f) for row in reader: self.facts.append(row) def _tokenize(self, text: str): if USE_JIEBA: return set(jieba.lcut(text)) # 简单正则分词或按字符切分仅作兜底 return set(text.strip().replace(, ).replace(。, ).split()) def search(self, query: str, top_k: int 3) - List[Dict[str, str]]: query_tokens self._tokenize(query) scored [] for fact in self.facts: content_tokens self._tokenize(fact[content]) # 计算交集词数量作为相关度分数 score len(query_tokens content_tokens) scored.append((score, fact)) # 过滤掉完全没命中的记录 scored [item for item in scored if item[0] 0] scored.sort(keylambda x: x[0], reverseTrue) return [fact for _, fact in scored[:top_k]]代码关键点FactFinder在初始化时读取全部事实记录_tokenize使用jieba分词若未安装则退化为简单切分search方法计算“用户问题”和“事实内容”之间的词汇交集分数越高的记录越相关避免返回零命中的结果为生成阶段省去无效信息。这个模块最大的优点是容易理解缺点是检索质量有限。生产环境建议改为向量检索例如使用sentence-transformers生成文本向量再计算余弦相似度。但无论底层如何检索模块对外都应返回统一的List[Dict]结构方便生成模块处理。4.2 生成模块基于事实生成草稿生成模块是核心。我们要通过 Prompt 引导模型只使用检索到的资料来生成标书内容并要求它输出结构化结果。创建src/generator.py文件内容如下# 文件路径src/generator.py import json from typing import List, Dict from openai import OpenAI class BidGenerator: def __init__(self, api_key: str, base_url: str None, model: str gpt-4o-mini): self.client OpenAI(api_keyapi_key, base_urlbase_url) self.model model def build_prompt(self, query: str, facts: List[Dict[str, str]], requirement: str ) - str: fact_text \n.join( [f[{fact[id]}] ({fact[category]}) {fact[content]} for fact in facts] ) prompt f 你是一名严谨的投标书撰写助手。你的任务是基于公司提供的资料库撰写标书内容。 【严格规则】 1. 你只能使用下方“资料库内容”中提供的信息禁止补充任何资料库中不存在的公司业绩、资质、证书、人员、合同金额等信息。 2. 如果资料库内容不足以回答用户问题你必须在“缺失信息列表”中明确说明缺少什么。 3. 不得编造日期、编号、金额、人名和项目名称。对无法确认的信息使用“待确认”或“资料库中暂未提及”。 4. 输出格式必须为 JSON包含三个字段draft、facts_used、missing。 【资料库内容】 {fact_text} 【用户问题】 {query} 【额外要求】 {requirement} 请输出 JSON 结果。 return prompt def generate(self, query: str, facts: List[Dict[str, str]], requirement: str ) - Dict: prompt self.build_prompt(query, facts, requirement) response self.client.chat.completions.create( modelself.model, messages[ {role: system, content: 你是一个严格基于事实库工作的助理。}, {role: user, content: prompt}, ], temperature0.2, ) content response.choices[0].message.content # 为兼容不同模型输出先简单清理可能出现的 Markdown 代码块标记 content content.strip() if content.startswith(json): content content[7:-3] elif content.startswith(): content content[3:-3] try: return json.loads(content) except json.JSONDecodeError: # 如果模型没有输出合法 JSON则返回原始文本交给校验层提醒 return {draft: content, facts_used: [], missing: [模型输出无法解析为结构化JSON]}这段代码中最关键的是build_prompt方法。它在提示词中明确列出了可依据的事实片段并给出了编号。模型被要求只使用这些事实而不是从自己的训练数据中“回忆”内容。temperature设为 0.2是为了降低生成随机性让输出更稳定。如果你的模型不支持 JSON 模式可以用正则从返回文本中提取字段或者要求模型输出纯文本再通过规则解析。工程上建议优先启用模型自带的 JSON 输出模式。4.3 后置校验模块拦截“漏网之鱼”后置校验是最后一道防线。我们要检查生成的文本中是否出现了资料库中不存在的关键信息。创建src/validator.py文件内容如下# 文件路径src/validator.py import re from typing import Dict, List # 常见的高风险信号词 RISK_TERMS [保证, 100%, 第一, 唯一, 最先进, 领先水平, 国家级] class BidValidator: def __init__(self, finder): self.finder finder def _extract_entities(self, text: str) - List[str]: # 简化实体抽取匹配连续的中文公司名、证书编号、金额 entities [] # 匹配证书编号如 QMS-2024-001 entities re.findall(r[A-Z]{2,5}-\d{4}-\d{3}, text) # 匹配金额如 280万元 entities re.findall(r\d\.?\d*万元, text) return entities def validate(self, query: str, result: Dict, facts: List[Dict[str, str]]) - Dict: draft result.get(draft, ) issues [] # 1. 检查高风险词汇这些词容易引起法律风险 for term in RISK_TERMS: if term in draft: issues.append(f发现高风险词{term}请人工确认是否存在夸大承诺。) # 2. 检查生成文本中的关键实体是否在检索到的facts中 fact_content .join([fact[content] for fact in facts]) entities self._extract_entities(draft) for entity in entities: if entity not in fact_content: issues.append(f检测到资料库中不存在的实体{entity}) # 3. 检查缺失信息列表 missing result.get(missing, []) if missing: issues.append(f模型提示缺失信息{, .join(missing)}) return { issues: issues, pass: len(issues) 0, needs_review: len(issues) 0, }这个校验模块不是万能的但它能拦截最明显的“编造实体”。比如模型生成了“QMS-2024-999”但资料库中只有“QMS-2024-001”validate就会提示异常。真实系统中可以接入更复杂的命名实体识别模型或者调用外部知识库进行交叉验证。4.4 组装完整流程现在把三个模块串联起来创建src/main.py# 文件路径src/main.py from finder import FactFinder from generator import BidGenerator from validator import BidValidator def main(): # 初始化模块 finder FactFinder(data/facts.csv) generator BidGenerator(api_keyYOUR_API_KEY, base_urlYOUR_BASE_URL, modelYOUR_MODEL_NAME) validator BidValidator(finder) # 模拟用户请求 query 请介绍一下公司在智慧园区项目上的经验。 # 1. 检索事实 facts finder.search(query, top_k3) if not facts: print(没有检索到相关资料无法生成内容请补充企业资料库。) return # 2. 生成内容 result generator.generate(query, facts) # 3. 后置校验 check_result validator.validate(query, result, facts) print( 校验结果 ) print(是否通过, check_result[pass]) print(待确认问题, check_result[issues] if check_result[issues] else 无) print( 生成草稿 ) print(result.get(draft, )) if __name__ __main__: main()在主流程中有一行关键判断如果检索结果为空直接中止生成而不是让模型自由发挥。这一步在真实系统中非常重要可以理解为“事实边界”的硬性拦截。5. 运行效果与结果说明5.1 示例输入输出我们以“请介绍一下公司在智慧园区项目上的经验”作为输入假设资料库中存在F003这条记录。检索模块大概率会召回[F003] 2023年完成xx市智慧园区平台建设项目合同金额280万元项目顺利验收。 [F006] xx智慧园区平台采用微服务架构支持设备接入、数据分析和可视化大屏。生成模块基于这两条记录可能输出类似下面的草稿公司于2023年完成了xx市智慧园区平台建设项目合同金额280万元并顺利通过验收。该项目采用微服务架构支持设备接入、数据分析和可视化大屏体现了公司在智慧园区领域具备一定的实施经验和技术积累。同时facts_used字段会带上[F003, F006]missing字段可能为空。此时校验模块不会发现明显的实体冲突可以进入人工复核环节。5.2 当资料库信息不足时的表现再换一个输入“请介绍公司在海外数据中心项目上的业绩。”如果facts.csv中没有海外项目相关记录检索模块很可能返回空列表。此时主流程会直接输出没有检索到相关资料无法生成内容请补充企业资料库。如果你希望在资料部分缺失时仍然生成一部分内容可以让模型输出{ draft: 公司目前具备一定的数据中心项目实施经验但资料库中暂未包含海外数据中心项目案例需要由项目负责人补充相关信息。, facts_used: [], missing: [海外数据中心项目案例] }这种“明说不知道”的写法恰恰是“拒绝说谎”在标书场景中的正确表现。它把信息缺口留给人工去补齐而不是用一段看似专业的空话掩盖风险。5.3 结果分析从这个简化例子可以看出生成内容的质量严重依赖两个因素事实库是否完善资料越全、更新越及时AI 发挥空间越大。检索结果是否准确搜不到最相关的事实模型就只能答非所问。因此后续优化重点应放在“清洗数据”和“提升检索精度”上而不是反复调 Prompt。当然调 Prompt 也能改善输出风格但解决不了信息缺失带来的编造问题。6. 常见问题与排查思路为了让其他同学少踩坑我把这类项目中常见的问题整理成一张表问题现象常见原因解决思路模型仍然编造业绩提示词约束太弱没有启用后置校验强化提示词规则并加入校验模块对高风险场景采用人工审批检索结果总是为空资料库的关键词与用户问题不匹配分词效果差增加同义词扩展使用向量检索替代关键词匹配模型输出格式不是 JSON模型版本较旧或不支持严格格式要求在提示词中给出 JSON 示例使用response_format参数解析失败则重试一次生成内容带有夸张承诺提示词中没有禁用词要求在系统提示词中增加“禁止使用保证、第一、唯一”等词汇校验模块加入风险词检测事实库中出现冲突信息同一项目多个版本未去重为每条资料增加有效时间和唯一编号建立事实版本管理敏感数据被泄露到生成内容资料库权限控制不足检索结果包含无关内容在检索前按用户权限过滤只返回最小必要资料片段实际排错时建议按照“先看检索结果再看 Prompt最后看模型输出”的顺序定位问题。大多数“AI 说谎”案例追到根因后都会发现是资料没检索到、资料本身错误或者 Prompt 没有强调边界而不是模型故意撒谎。7. 工程化最佳实践与安全建议7.1 数据质量决定生成边界不要一开始就追求复杂的模型策略先把资料库管理好。企业资料通常分散在 Word、PDF、Excel 和内部知识库中需要一套流程把它们清洗成带唯一编号、拥有人、更新时间的结构数据。每一条事实最好都能关联原始文件这样人工复核时能一键查看依据。对于会失效的信息例如资质证书有效期、人员社保缴纳记录要设置定期复核提醒避免 AI 引用过期资料。7.2 从“提示词约束”到“系统约束”提示词约束虽然简单但它属于“软约束”模型可能在不同措辞下遗忘规则。系统约束则包括无法检索到资料时强制终止生成生成内容必须通过实体校验否则自动标记为“需人工修改”对于包含“承诺”“保证”“金额”“日期”的句子必须展示对应事实依据否则不得进入正式文档。在系统设计上要给每条生成记录保留完整的链路追踪信息。也就是说某个标书段落是谁在什么时间、基于哪几条资料、由哪个模型生成、经过了哪些人工修改都要能查出来。这个审计能力在招投标合规中非常关键。7.3 权限控制与数据安全投标书通常包含不少企业内部敏感信息例如报价、人员名单、项目细节。开发这类系统时要遵循最小权限原则不同角色的用户只能检索自己有权访问的资料库大模型 API 调用要避免把完整企业库批量发送给外部服务优先评估私有化部署或本地模型方案使用云端模型时确认数据加密与日志留存策略。如果涉及生产环境变更例如修改资料库或更新提示词应先在测试环境验证再发布到生产并保留回滚方案。任何删除资料、批量导入、重新生成的操作都必须有操作记录和备份。7.4 评估模型“诚实度”的指标想评估系统有没有真正做到“拒绝说谎”可以关注下面几个指标指标含义计算方法可溯源比例生成内容中有多少句子能对应到具体资料人工抽样或自动比对引用编号缺失项回召率资料库中确实缺少的信息模型是否主动报告人工构造缺失问题统计模型报告比例实体幻觉率生成文本中出现的资料库外实体数量用实体识别和资料库做差集人工修改率生成草稿需要人工修改的比例统计审核环节改动次数不建议只看“内容是否流畅”或“是否像真标书”。一个没有依据但看起来很专业的回答反而是风险最大的结果。8. 总结与下一步学习建议围绕 “Making an AI bid writer refuse to lie”本质上是一个典型的“大模型对齐 检索增强 内容审计”工程问题。本文完整实现了一个最小闭环用FactFinder限定资料范围用BidGenerator把“只用资料库内容”写进提示词用BidValidator做输出校验最后把三条链路串成可运行的主流程。这套设计思路不限于标书也适用于合同生成、合规报告、产品文档等需要强事实约束的场景。如果你正准备在自己项目中落地我建议先不要直接做全量标书生成而是选一个风险最低的章节比如“公司简介”或“项目团队介绍”先用小范围资料库跑通流程。重点观察三个环节资料检索是否准确、模型是否频繁输出缺失项、人工复核需要多长时间。当这三个指标都稳定后再逐步扩大到技术方案、业绩证明等高风险模块积累到足够多的真实资料和标注数据后再考虑引入更复杂的向量检索和微调方案。最后提醒一句技术手段只能降低“AI 说谎”的概率无法完全消除。越是严肃的商业文档越要保留人工决策和责任人签字环节。把 AI 当作一个“有边界感的草稿助理”比让它假装全知全能安全得多。