一、先讲个场景,你就知道问题在哪了
我拿一份真实的招标文件,加上一份 40 万字的历史施组,丢给 AI 让它写技术标。
它写出来了。章节结构对、专业术语对、行文语气也对——看起来像那么回事。
然后我盯着屏幕,一个字都不敢用。
原因特别朴素:
我不知道哪里是它编的。
一本施工组织设计几十万字。摆在面前的选择只有两个——要么相信它(我不敢),要么逐字核对(那我不如自己写)。
生成提速 10 倍,核对也要花 10 倍时间。这笔账算下来等于白干。
这就是「AI 写标书」这件事真正的死结。它跟模型强不强关系不大——今天任何一个头部模型都能写出通顺的施组。卡点在后面那一步:你敢不敢签上名字交出去。
所以这个项目(BidCraft 标书匠,Apache-2.0,完全免费开源,无付费墙也没有企业版)从一开始就不是奔着「一键生成」去的,而是奔着解决这个死结去的。
演示项目是虚构的(云岭至江口高速公路 YJ-3 合同段),不含任何真实工程资料,下面几张截图同理。
二、我换掉的那个标准
一开始我的目标是:让它尽量别写错。
后来发现这目标是错的。AI 写几十万字专业文档,在可预见的未来都不可能做到「全部正确、人不用看」。追这个目标,只会得到一堆花哨但不敢用的功能。
所以我把它换掉了:
别追求「不错」,追求「错了你能一眼看见」。
换完之后,衡量标准就变得非常具体,只有一句话:
打开生成的正文,需要我动手改的地方有多少。
下面所有做法,都只服务于这一件事。而它们分布在三个时间点上——写之前、写的时候、写之后。这篇文章就按这个顺序讲。
三、写之前:把数字从正文里拆出来
这是所有措施里最重要的一条,因为它解决的不是「幻觉」,是一个更容易被忽略的问题。
问题的真相
同一份施组里,「总工期」这个数字可能出现在十几个地方。
如果每次都让模型从原文里「读」一遍这个数字——那么这十几个地方,就可能出现十几个版本:26 个月、780 天、2026 年 3 月至 2028 年 4 月……
注意,这不是幻觉。这是「每次重新理解」必然导致的不一致。
模型每次都很忠实,它只是每次都在重新读、重新理解。你怪不到它头上。
做法:数字不进正文,进一张表
所以我把这类数字全部抽出来,单独存一张 global_fact 表:
| 字段 | 作用 |
|---|---|
fact_key / fact_value / unit | 事实本身(如 总工期 = 26 个月) |
source_file / source_location | 出处:哪份文件、哪一节/哪一页 |
confidence | 抽取置信度 |
applicable_lots | 适用范围(全线通用 / 指定标段) |
category | 分类(工期 / 人员 / 机械 / 造价 / 地质 / 结构…) |
status / edited / version | 状态、是否人工改过、版本号 |
生成正文时,全部事实作为权威数据一股脑注入,模型的指令只有一句:数字必须来自事实表或本项目文件原文。
有三个设计点我认为很关键,都不是为了「更智能」,而是为了「更可信」:
① 每条数字都带出处。 不是「AI 说的」,是「招标文件 · 第一章 招标公告」。核对的时候你查的是出处,不是一个置信度分数。
② 人工改过的,重抽取时不会被覆盖。 改过的条目 edited=1,AI 后续重跑保留你的值。否则你辛苦改一遍,下次重新解析全被冲掉——那没人愿意改。
③ 值变了,旧版本标 expired、版本号 +1。 历史可追溯。谁在什么时候把 26 个月改成了 30 个月,查得到。
左上「全局参数」抽屉:每条事实都带出处,可编辑、可删除,可按标段筛选。
这条措施的价值在于:它把「不确定」从生成环节前移到了校对环节,而且只前移了一次。
十几个地方引用同一个数字,你只需要在事实表里确认一次。
四、写的时候:三条硬规则
数字管好了,接下来是正文生成本身。我没有靠「更好的 prompt 措辞」,而是把三条规则写死进了 system prompt,让模型没有自由发挥的空间。
规则一:找不到依据,就写 [待补充]
这是整个项目里降幻觉最有效的一条。原文是这样的:
所有数字必须来自「本项目事实」或「本项目文件原文」;两者都找不到的数值一律标注
[待补充],禁止借用历史知识页/历史原文里的数值。
为什么这条比「请不要编造数字」有用?因为它把「幻觉」转化成了「待办事项」:
| 模型的行为 | 你付出的代价 |
|---|---|
| 编一个数字 | 你无法察觉,直到出事 |
标一个 [待补充] | 你一眼看到,去查一次,填上 |
宁可留一个显眼的空,也不要填一个看不出问题的错。
而且 [待补充] 在编辑器里是可编辑的底色标记(不是装饰性样式)。于是你的工作流就变成了:
搜索「[待补充]」 → 逐个填 → 完事
而不是:
通读几十万字 → 试图从中找出哪句话不对 → 找不到
这就是第二节那个新标准的落地——把「通读找错」换成「搜一个词、改几处」。
规则二:冲突了听谁的,写死一条链
当不同来源的数字打架时,如果不给明确的优先级,模型会随机挑一个。所以裁决链是硬编码的:
本项目文件原文 > 本项目事实表 > 历史知识页 / 工法
这里有个反直觉的地方值得说一下:第一名的「文件原文」排在「事实表」前面。
因为事实表本身就是从文件里抽出来的。如果两者冲突,说明抽取环节可能有误——这时候当然要信原文,而不是信我那张二手表。
顺带提一句,上下文注入还有另一条优先级(谁的话更该听):
本次用户特别要求 > 用户经验偏好 > 项目文件原文 > 事实表 > 知识页/工法
规则三:历史资料——给风格,不给数据
历史施组是最好的写作范本,但同时也是最危险的幻觉来源,因为里面的数字全是别的项目的。
所以对历史资料我做了严格的分层使用:
| ✅ 可以借鉴 | ❌ 严禁照搬 |
|---|---|
| 行文结构、组织顺序 | 项目名、标段号 |
| 专业措辞、通用工艺描述 | 里程桩号、日期工期 |
| 通用质量/安全/环保措施 | 工程量与数量、造价 |
| 通用管理要求 | 人员机械台数、工点名称 |
一句话:不依赖具体项目的文字可以借鉴,一切项目特定且可量化的内容严禁照搬。
这条规则有个很有意思的副产品。我原本以为,把历史章节的「编制方式/要点」结构喂给模型,会让它写得更好。
实测结果相反:一旦给了结构,模型会连着结构里的历史数字一起搬过来,写出来的东西和历史上那一章高度同构,完全没有本项目的针对性。
所以最终方案是「给风格,不给内容;给形状,不给数字」——只给那章历史原文的完整文本做行文示范,加一份表图清单(这里该配什么表、什么图),但历史表格里的数据一个都不给。
正文流式落进所见即所得编辑器,顶部是「写思路 → 编写正文 → 完成」的推进流程。
五、写之后:让人能查证
前面的措施都是「生成时」的约束。但人不可能完全信任一个黑箱,所以「生成完」还得解决三件事。
① 这一章到底用了什么素材,点开就能看
正文落库的时候,同时存一份取材摘要:
{
"experiences": [{"id": 12, "title": "…"}],
"knowledge_pages": [{"id": 317, "title": "隧道工程 > 超前地质预报"}],
"file_snippets": [{"file": "招标文件.pdf", "section": "第三章 评标办法"}],
"methods": [{"id": 8, "name": "超前地质预报施工工法"}],
"fact_count": 23
}
编辑器里可以点开「取材来源」,看到这一章每条素材的原文。
它的价值不在于「更透明」这种虚词,而在于 核对时不用通读全文,只查你怀疑的那几段引自哪里。而且一旦发现某段写得不对,你能直接定位到是什么素材导致的——是素材本身的问题,还是匹配逻辑选错了素材。
可解释性是信任的前提。你敢用生成结果,是因为你随时能查证。
② 能确定算出来的,一行都别让模型算
模型不擅长的事,不要勉强它。这个项目里凡是能确定计算/判断的,全部交给代码:
| 环节 | 交给代码做 |
|---|---|
| 事实去重 | 按 (category, fact_key) 归并,取高置信度 |
| 事实版本管理 | 值未变跳过;值变则旧行 expired + version+1 |
| 标段过滤 | 按 applicable_lots 精确匹配,不靠模型记得 |
| 父子章节去重 | 同选时只留父节点 |
| 评分类章节过滤 | 正则剔除「评标/须知/前言/致谢」类章节 |
| 表格行级裁剪 | 多标段整表按本标段词表裁行 |
| 序号 → ID 映射 | 给模型看序号,程序映射回真实 ID |
举一个我认为最典型的例子:评分项。
招标文件的评分办法结构化提取之后,程序会做覆盖完整性检查与分值总和核对——因为评分项漏一条,可能导致直接废标。
这种事情绝对不能交给模型「自觉」。模型会非常自信地告诉你「我已核对完毕,合计 100 分」,而实际上它可能漏了一条 5 分的项。
程序算一遍,1 秒钟的事。
③ 对话式还有个更阴的坑:它谎称「做完了」
正文生成的幻觉是「编内容」,而对话链路的幻觉是 「谎称已完成」——后者更致命,因为它会让你以为事情做好了,从而不再检查。
我见过模型说「第三章已生成完毕」,但实际上工具根本没被调用成功。
对话式编制,工具执行过程可展开核对。
三层防护:
- 定稿核验——模型声称「已生成/已写入」,必须有本轮真实的工具成功记录背书,否则否决重做
- 剥除伪造痕迹——模型自己编的「[已执行工具 ✓]」标记会被主动剥掉
- 防模仿——历史消息不注入工具标注,避免它照着格式伪造
另外还有个我觉得挺好用的小设计:工具遇到错误时返回真实数据,而不是返回报错。
比如模型传了个不存在的章节 ID,工具不会甩一个异常,而是返回「当前项目实际节点如下:…」。模型下一步就能自我修正。
把错误变成可自愈的信息,而不是让用户重说一遍。
六、回过头看,整件事其实只做了一次转化
绕了一圈,这套东西的本质可以压缩成一句话:
把「我需要通读几十万字找错误」,变成「我搜一个关键词,改几处」。
- 写之前,数字抽进事实表 → 十几个引用点,你只需确认一次
- 写的时候,没依据就标
[待补充]→ 错误从「看不见」变成「显眼的空」 - 写之后,取材来源可查 + 确定性的交给代码 → 出问题能定位,不会算错
这三个动作指向的是同一个目标:不是让 AI 不犯错,而是让它的错处变得便宜。
七、但这套东西解决不了什么(诚实版)
我不想只讲好的一面。下面这些是它做不到的:
- 模型理解错误。它可能把「桥面铺装」理解成「桥面防水层」,整段写得完全跑题。这类错误只能靠人工审。
- 专业判断分歧。某个方案是否合理,模型给的答案可能和资深工程师不一致——这是判断题,不是知识题。
- 长文档的一致性衰减。整本几百页的一致性,靠逐章生成是保证不了的,需要最后的通读环节(这也是「质检智能体」还在 roadmap 上的原因)。
- 原文本身有错。如果招标文件里就有矛盾,模型会忠实地把矛盾抄下来。
但可以确定的是:人工核对的工作量会明显下降。
不是从 100% 降到 0%,而是从「逐字读几十万字」降到「查关键数字 + 审专业判断」。
在标书这个场景里,这个降幅已经足够有价值了。
八、开源信息
- Apache-2.0 —— 可商用、可修改、可闭源分发,唯一要求是注明来源
- 完全免费,无付费墙、无企业版、无功能阉割
- 数据全部在你自己服务器上,除了你主动调用的 LLM 和文档解析服务,没有数据外流
- 不锁定模型——走 OpenAI 兼容接口,改个
LLM_BASE_URL就能换任何厂商
技术栈:FastAPI + Vue 3 + TipTap + LangGraph + MySQL + MinerU + 通义千问
部署最省事的方式:把仓库丢给你的 AI 编程助手。项目自带 AGENTS.md,Claude Code / Codex / Cursor / Trae 都会自动读取,你只要说一句:
「按 AGENTS.md 把这个项目跑起来」
它就会自己装依赖、配环境、建表、起服务。缺的 API Key 它会开口问你要,不会瞎编一个。
仓库里还有个 思路/ 目录,把上面这些决定展开成了 7 篇完整复盘,每篇都写了取舍、代价和踩过的坑——包括这篇没展开的「怎么让知识命中得更准」和「怎么让内容不重复」。
如果这个项目对你有用,欢迎点个 Star。
也欢迎来 Issue 聊聊——尤其是「该不该用 RAG」这件事,我现在越来越觉得答案取决于你的文档是不是强结构,而这个判断我肯定还没想到所有情况。