让AI标书生成器拒绝编造:基于事实库与RAG的工程化方案
2026/9/6 14:07:48 网站建设 项目流程

在招投标文档自动生成项目里,用大模型写标书有一个绕不开的痛点:模型会把不存在的项目业绩、资质证书、人员履历写得“有鼻子有眼”。看似节省了人力,实际上埋下了废标、资质造假、法律纠纷等隐患。本文围绕 “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用于调用大模型接口;
  • pandascsv用于管理事实资料表;
  • 检索模块为了便于演示,采用中文分词和关键词打分实现;实际上可替换为向量数据库或 Elasticsearch。

需要特别说明的是,不同项目的大模型版本、接口参数差异很大,示例代码中的模型名称、API 地址需要根据你自己的环境调整。文章重点强调的是工程链路,而不是某个具体版本。

3. 环境准备与工程目录

3.1 运行环境

本文示例在普通开发机上即可运行。建议环境如下:

  • 操作系统:Windows / macOS / Linux 均可;
  • Python 版本:3.9 或以上;
  • 大模型 API:需要你有可用的 OpenAI API Key,或兼容 OpenAI 协议的服务地址;
  • 额外依赖:pandasopenai,如果使用分词需要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", encoding="utf-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(key=lambda x: x[0], reverse=True) 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_key=api_key, base_url=base_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( model=self.model, messages=[ {"role": "system", "content": "你是一个严格基于事实库工作的助理。"}, {"role": "user", "content": prompt}, ], temperature=0.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_key="YOUR_API_KEY", base_url="YOUR_BASE_URL", model="YOUR_MODEL_NAME") validator = BidValidator(finder) # 模拟用户请求 query = "请介绍一下公司在智慧园区项目上的经验。" # 1. 检索事实 facts = finder.search(query, top_k=3) 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 当作一个“有边界感的草稿助理”,比让它假装全知全能安全得多。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询