# 公司知识库(Hesu Auto 事实卡)实施方案 > **状态:方案定稿——四个待确认问题已全部确认(2026-09-10),待批准执行。** **Goal:** 给 AI 客服补充公司事实知识(品牌/覆盖/网络/经验),使客户问公司情况时**零编造、简短、可信**,且不改变职能边界(不报价/不承诺/不报联系方式)。 **Architecture:** 新建独立事实卡文件 `app/prompts/company_facts.md`(知识与行为分离),由 `ai_service.py` 拼接进对话 prompt(+3 行 loader;顺序:system.md 行为 prompt → 事实卡 → 当轮 context 提醒);事实卡内容经输出墙安全审查(每句过价格墙+零号码墙);不动 extractor/evaluator/前端/配置。 **Tech Stack:** 零新依赖、零新基础设施。纯 prompt 文件 + 2 行 loader 改动 + 静态安全测试。 --- ## 1. 背景与问题 现状:`system.md` 是纯行为 prompt,**不含任何公司事实**。客户问"你们是什么公司/有哪些品牌/送不送到肯尼亚/是不是真的"时,GLM 只能编造。这正是 2026-08-20 changelog 记录过的失效模式——**"prompt 不含真值 → GLM 编造假号"的同款问题,只是从号码换成了公司事实**(编造品牌/国家/年限=信任崩塌,客户流失)。 **生产实证(2026-09,Ghana 案例)**:客户问 "I'm in Ghana, do you have branch in Ghana",AI 答 **"Not yet, but we've got partners worldwide..."**——而公司**在 Ghana 有分支**(五分支之一,且有保税仓)。客户就在 Ghana、公司就在当地有网点,本是信任建立的绝佳时机,被知识真空变成当面否认。诊断:GLM 对无法验证的事实问题选择"听起来合理的否定+转进"(hedging),**否定性编造与肯定性编造同样致命**;且此类问题("do you have branch in X")没有特征触发词(只有国名),触发词注入方案(C)无法覆盖——这是选型 A(事实常驻)的最强论据。 **编造面全量盘点(2026-09-10,用户发起)**:除公司事实外又发现两个编造向量,均折入 Task 3/6—— 1. **库存/现车断言**:system.md 红线禁"库存承诺",但其"好回复"示例有三处在教 AI 说 "we got tons of them"/"in stock"——本会话已两次实证 GLM 听逐字示例不听抽象红线,示例教唆必被模仿;AI 无任何库存数据源,照示例回答=编造现货。(修正:Task 3 改动 3,按文本锚点定位) 2. **保修/质保承诺**:价格墙时间模式显式豁免 warranty(`(?!\s*(warranty|质保|保修))`,[boundary_filter.py:22](app/services/boundary_filter.py#L22))——AI 若输出编造的 "6 months warranty" 承诺,墙级零拦截(实证)。**修法升级:Task 6 墙级收紧**(prompt 红线 Task 3 改动 4 为 belt)。复查中还发现**中文时限/质保承诺("3天到货"/"6个月质保")今天全部零拦截**(时间模式只有英文单位)——Task 6 一并补。 其余编造面(车型公共参数过期、认证资质)接受为观察项,见 §6。 知识库要解决的四类真实问题: | 客户问题类型 | 例子 | 需要的事实 | |---|---|---| | 信任核实 | "are you a real company?" | 首批出口企业/集团背景/累计出口量 | | 品牌/产品 | "do you have ZEEKR? trucks?" | 30+ 品牌清单/动力类型/车型类别 | | 覆盖范围 | "do you ship to Ghana?" | 70+ 国家/五分支/Dubai 枢纽 | | 运作模式 | "how does this work?" | 中国采购→迪拜枢纽→海外交付 | ## 2. 方案选型 | | A. 静态事实卡(推荐) | B. RAG 检索 | C. 触发词条件注入 | |---|---|---|---| | 做法 | 事实卡文件常驻 prompt | 向量/关键词检索,按轮注入相关块 | 仿 CONTACT_ASK_TRIGGER_RE,问公司才注入 | | 优点 | 零新设施;事实常在,漏触发=零;单 commit 回滚;与现有架构(prompt 驱动)完全一致 | token 省;可扩展到车型库存/FAQ | token 省 | | 缺点 | 每轮 +~700-900 token(实测草案 1289 chars;glm-4-flash 成本可忽略) | 需引入检索设施(本项目零基础);**检索漏=编造风险回归**;新失效模式 | 触发漏(客户间接问)→ 编造风险回归 | | 结论 | ✅ **本期采用** | KB 长大到库存/FAQ 规模再议 | 事实卡膨胀后再优化,loader 设计已留位 | **选 A 的决定性理由**:本项目的可靠性哲学是"确定性优先"——RAG/触发词都引入"该注入时没注入"的 probabilistic 失效,而编造的代价(信任崩塌)远高于每轮几百 token。 ## 3. 事实卡内容草案(即 Task 1 的文件内容) ``` # 公司事实卡(仅供回答客户关于公司的事实性问题) ## 我是谁 - Hesu Auto 是 Hesu Group 旗下专注汽车出口的公司 - 中国首批汽车出口企业之一,累计出口 50,000+ 台(用户已确认,2026-09-10) - **累计出口到 70+ 国家与地区**(中东、非洲、南美、欧洲)——⚠️ 出口目的国口径,不是分支/网点覆盖(用户明确区分,2026-09-10) - 与 30+ 汽车品牌深度合作:BYD、Changan、Geely、Leopard、ROX、Nissan、Zeeker、Li Auto 等 ## 卖什么 - **新车与二手车都做**(用户已确认) - 全动力类型:燃油、混动、插混、纯电 - 全车型类别:轿车、SUV、皮卡、重卡;乘用车、商用车、新能源车、车队/项目采购 ## 网络与运作 - 模式:中国采购供应 → 迪拜(Jebel Ali)枢纽备货分拨 → 海外交付 - 海外分支五处:Dubai、Ghana、Ethiopia、Djibouti、Belarus - Abu Dhabi 有展车节点;Ghana 西非保税仓;Djibouti+Ethiopia 东非协同;Venezuela(南美)在网络节点内(用户确认列入) ## 使用规则(与 system.md 红线同级) - **统一兜底(最高优先,用户定调 2026-09-10)**:任何不清楚、事实卡没有、需要核实的问题——**不编造、不承诺、不否定、不断言**,统一动作只有一个:顺势引导留联系方式转客户经理——"这个我让我们经理给你准确答复——留个 WhatsApp,他直接联系你" - 客户问公司情况/品牌/是否可信时,取上述事实**简短作答(1-2 句)**,建立信任 - **分支问题与出口覆盖问题是两回事,分开答**: - 问分支/网点("do you have branch in X")→ 只认卡里列的:五分支+节点(Dubai/Ghana/Ethiopia/Djibouti/Belarus/Abu Dhabi/Venezuela)必须答"有"并点出当地节点;卡里没有的 → **走统一兜底**——绝对禁止编造否定答案("Not yet"/"we don't have"/"没有")(Ghana 案例教训:公司明明有分支被答成 Not yet) - 问出口覆盖("do you ship to X")→ 70+ 国是**累计出口目的国**,不代表 X 一定有网点:答"已出口到 70+ 国家"事实 + **走统一兜底**(X 具体情况经理确认)——不承诺、不否定 - 绝不主动长篇介绍公司,绝不用成营销腔——风格仍按【说话风格】 - 涉及具体价格/交付时效/库存承诺 → 仍按【职能边界】转客户经理,本卡不改变任何红线;售后只说"有本地协同支持",不做任何保证 - 事实卡没写的细节(某国代理、某车型现车、认证资质细节)→ **走统一兜底**(不编造,留资+经理沟通) - 数字一律照抄本卡写法(如 50,000+、70+),不改写成中文数字 ``` ### 内容取舍说明(对照原始资料) | 原资料内容 | 处置 | 理由 | |---|---|---| | About 正文全篇 | 压缩为"我是谁"4 行 | 营销语气("strives to win recognition")违背说话风格;KB 只要事实 | | Contact Us 节 | **整节剔除** | 红线:AI 绝不输出我方联系方式(web 指按钮/App 反向留资,2026-08-20 已定) | | "Why Work with HESU" 四点 | 融入"网络与运作"1 行 | 与 Global Network 高度重复,留骨架 | | Key Facts "10,000+" | 不采信 | 与 About "50,000+" 矛盾,用户确认以 **50,000+** 为准 | | "72+ Countries" | 取 "70+" | About 正文"more than 70"更稳,两处兼容 | | "Zeeker" | **保留原文 Zeeker**(用户确认) | 用户口径为准,不作 ZEEKR 规范化 | | Venezuela | **列入**(用户确认) | 仅网络图出现,按用户口径列为网络节点 | | 新车/二手车定位 | **新旧车都做**(用户确认) | 人设第一句同步改为"汽车出口外贸销售(新车与二手车都做)",Task 3 一并处理 | | Leopard、ROX 品牌名 | **保留原文**(用户确认) | 照抄入卡:Leopard、ROX | ## 4. 与现有防线/风格的兼容性审查(本方案的核心约束) | 审查项 | 结论 | 说明 | |---|---|---| | **价格墙** `violates_boundary` | ⚠️ 有陷阱,已规避 | `五万` 这类**中文数字+万**会被裸预算模式击杀(`(两|三...十)\s*万`),AI 引用即被墙拦截替换成迂回话术——张冠李戴。**事实卡强制阿拉伯数字**(50,000+),Task 2 的墙扫描测试会把关 | | **零号码墙** `no_contact_value` | ✅ 安全 | 卡内数字均 <10 位(50,000 逗号断开=5 位);无邮箱;Task 2 扫描钉死——**同时防止未来有人往 KB 里加电话/邮箱** | | **再索要墙** `asks_contact` | ✅ 交互正确(实证) | 使用规则里的兜底话术"留个 WhatsApp,他直接联系你"**命中再索要墙**——客户已留资时 GLM 复读兜底话术会被正确丢弃(墙换成"经理主动联系"版),未留资时放行(合法引导)。两层设计自洽,无需处理 | | **职能边界** | ✅ 显式锁定 | 使用规则第 3 条:事实不改变报价/承诺转接;交付措辞避免时限含义("faster delivery"不进卡) | | **说话风格** | ✅ 规则化 | 1-2 句简短作答,禁营销腔;品牌/地名保留英文(GLM 按客户语言自然组织) | | **extractor/evaluator** | ✅ 不受影响 | 两者用独立 prompt,不拼事实卡;extractor 只提取客户信息 | | **token 成本** | ✅ 可忽略 | 实测草案 1289 chars ≈ 700-900 GLM token/轮(glm-4-flash 成本仍可忽略);换"事实常在零漏触发" | | **回滚** | ✅ | 事实卡文件+2 行 loader,单 commit revert | ## 5. 任务分解 ### Task 1: 事实卡文件 + loader 拼接(TDD) **Files:** - Create: `app/prompts/company_facts.md`(§3 草案,按 §7 确认结果定稿) - Modify: `app/services/ai_service.py`(+3 行) - Create: `tests/test_company_facts.py` - [ ] **Step 1: 失败测试——prompt 组装顺序** ```python """公司事实卡: 拼接位置与墙安全性静态测试。""" from pathlib import Path from app.services.ai_service import AIService, COMPANY_FACTS, SYSTEM_PROMPT FACTS_PATH = Path("app/prompts/company_facts.md") class _CapturingProvider: def __init__(self): self.prompts = [] async def chat(self, messages, system_prompt): self.prompts.append(system_prompt) return "ok" async def stream_chat(self, messages, system_prompt): self.prompts.append(system_prompt) for t in ("ok",): yield t def test_facts_concatenated_between_system_and_context(): # 顺序: 行为 prompt → 事实卡 → 当轮提醒(提醒必须最后,保持最新鲜) svc = AIService(provider=_CapturingProvider()) svc.provider.prompts.clear() import asyncio asyncio.run(svc.chat([{"role": "user", "content": "hi"}], context="⚠️ 提醒")) p = svc.provider.prompts[0] assert "Hesu" in p # 事实在场 assert p.index(SYSTEM_PROMPT) < p.index(COMPANY_FACTS) < p.index("⚠️ 提醒") def test_facts_absent_from_extractor_prompt(): # extractor/evaluator 不拼事实卡(它们有自己的 prompt,不该看到公司事实) from app.services.ai_service import EVALUATOR_PROMPT, EXTRACTOR_PROMPT assert "Hesu" not in EXTRACTOR_PROMPT assert "Hesu" not in EVALUATOR_PROMPT ``` - [ ] **Step 2:** `python -m pytest tests/test_company_facts.py -v` → FAIL(两个测试均以 ImportError/收集错误失败——`COMPANY_FACTS` 是模块级常量且 company_facts.md 未建,属预期红灯形态;本文件仅此两测试,无部分通过预期被破坏) - [ ] **Step 3: 实现** `ai_service.py`: ```python SYSTEM_PROMPT = _load_prompt("system.md") COMPANY_FACTS = _load_prompt("company_facts.md") # 公司事实卡(知识与行为分离) EXTRACTOR_PROMPT = _load_prompt("extractor.md") EVALUATOR_PROMPT = _load_prompt("evaluator.md") # 对话基座: 行为 prompt 在前(红线显著性),事实卡居中(参考资料), # 当轮 context 提醒最后拼接(在 chat/stream_chat 内,保持最新鲜) _BASE_CHAT_PROMPT = SYSTEM_PROMPT + "\n\n" + COMPANY_FACTS ``` `chat`/`stream_chat` 内 `SYSTEM_PROMPT + "\n\n" + context` 改为 `_BASE_CHAT_PROMPT + ("\n\n" + context if context else "")`。 创建 `app/prompts/company_facts.md`(§3 定稿内容)。 - [ ] **Step 4:** 测试通过;全量回归 `python -m pytest tests/ -q` 全绿(基线 160) - [ ] **Step 5: 提交** `feat(kb): 公司事实卡入 prompt——Hesu 品牌/覆盖/网络零编造` ### Task 2: 事实卡墙安全静态测试(TDD) - [ ] **Step 1: 失败测试**(追加到 `tests/test_company_facts.py`) ```python from app.services.boundary_filter import ( no_contact_value, split_sentences, violates_boundary, ) def _fact_sentences(): # 注: 事实卡是无句读的 markdown 列表,多行会合并成大句块——无害, # 检测是 search 式,违规在任何块内都能命中(§3 草案独立评审实测 1289 chars # 过两墙全清——长度数字随草案修订变化,以实施时实测为准) complete, tail = split_sentences(COMPANY_FACTS) return complete + ([tail] if tail else []) def test_facts_pass_both_output_walls(): # AI 原样引用事实也必须安全: 每句过价格墙+零号码墙 # (陷阱实证: "五万"会被裸预算模式击杀、"累计出口五万台"→整句被替换成要号话术) for s in _fact_sentences(): assert not violates_boundary(s), s[:60] assert not no_contact_value(s), s[:60] def test_facts_length_capped(): # 防营销手册化: 事实卡是卡片不是 brochure——超限=该精简或走检索方案 assert len(COMPANY_FACTS) < 2500 def test_facts_no_contact_channel(): # 我方联系方式绝不入卡(红线: web 指按钮/App 反向留资) # 只查确定性标记: 邮箱@与 wa.me 链接("WhatsApp"品牌词是合法内容,不作标记) assert "@" not in COMPANY_FACTS assert "wa.me" not in COMPANY_FACTS.lower() ``` - [ ] **Step 2:** FAIL(文件未建/或内容违规)→ **Step 3:** 按 §3 定稿 → **Step 4:** 全绿 - [ ] **Step 5: 提交** `test(kb): 事实卡墙安全静态网——中文数字陷阱/联系方式/长度上限` ### Task 3: system.md 集成(人设定调 + 事实作答口径 + 编造面封堵) - [ ] **改动 1——人设第一句**(业务定位用户已确认"新旧车都做")。⚠️ 该句是 system.md L1 的**前缀**(同行后续为"你说话像在 WhatsApp…"风格句)——做**子串替换**,不要整行替换删除风格句: 原(子串): ``` 你是一位二手车外贸销售 ``` 改为: ``` 你是一位汽车出口外贸销售(新车与二手车都做) ``` - [ ] **改动 2**——在【职能边界】"三类请求的处理"第 1 类(汽车信息)后追加一行: ``` - 公司情况(品牌/覆盖国家/出口经验/运作模式)按系统提供的事实卡简短作答建立信任——但不因此报价、承诺或跳过留资引导 ``` - [ ] **改动 3——库存断言示例修正(三处,按文本锚点定位,勿信行号)**:示例在教学"现货断言",而 AI 无库存数据源(编造向量,见 §1 盘点)。保留各示例的原本教学意图(围绕客户说的车型/年份),只把现货断言改为"可采购调配"。⚠️ 行号会随文件改动漂移(方案撰写时实测在 L105/L111/L117,上轮修复后已漂移过一次)——实施时以 `grep -n "in stock\|tons of them" app/prompts/system.md` 定位,以下原文为准: | 原文(逐字) | 改为 | |---|---| | "Corolla is a solid pick, we got tons of them. Any preference on year?" | "Corolla is a solid pick, we can source it easily. Any preference on year?" | | "For 2025 models we got BYD Han, Toyota Camry, Honda Accord in stock. Budget range?" | "For 2025 models we can source BYD Han, Toyota Camry, Honda Accord. Budget range?" | | "We got the 2024 Tang EV 7-seater and the DM-i AWD in stock. Want specs?" | "The 2024 Tang comes as the EV 7-seater and DM-i AWD — we can source either. Want specs?" | ⚠️ 差示例(L104 附近,"我们在非洲和东南亚市场有大量现车")是反面教材,**不要**顺手修改——它被标为"差"正是因为客服腔,保留其反例作用。 - [ ] **改动 4——红线区补三条**(价格墙对 warranty 形态白名单放行,保修承诺只能靠 prompt 封;Task 6 墙收紧为确定性兜底): ``` - 一切不清楚、没把握的问题(卡外国家/库存/保修/认证/任何事实细节)——不编造、不承诺、不否定、不断言,引导客户留联系方式,由客户经理沟通 - 不对库存/现车做断言——有没有现车、什么时候有,由客户经理确认;你只能说"可以采购调配" - 不报任何保修/质保期限("X months warranty"这类数字承诺)——一律"客户经理会给你完整的保修方案" ``` - [ ] 回归 `python -m pytest tests/ -q` 全绿(prompt 改动无单测,靠既有安全网;注意 test_boundary_filter.py 的 `test_whitelist_clean` 用例含 "we got tons of them" 作为墙输入——那是测试自有字符串,不依赖 system.md,不受影响) - [ ] **提交** `feat(prompt): 人设定调+事实作答口径+库存/保修编造面封堵` ### Task 4: 改动文档 + 冒烟清单 - [ ] 新建 `docs/2026-09-10-company-knowledge-base-changelog.md`(体例同前两份:背景/选型/文件清单/已知限制/部署回滚/冒烟结果) - [ ] **真机 GLM 冒烟(部署前置项,单测覆盖不到 GLM 引用事实的方式)**: | 场景 | 期望 | |---|---| | **"I'm in Ghana, do you have branch in Ghana?"(生产案例复现)** | **必须有分支**——"Yes, we've got a Ghana branch with bonded storage" + 顺势问需求;绝不再说 Not yet | | "are you a real company?" | 1-2 句事实作答(首批出口企业/出口量/70+ 国),零编造零营销腔 | | "do you have Zeeker? pickup trucks?" | 按卡答(品牌清单;皮卡重卡都有),不报库存承诺 | | "do you ship to Kenya?"(卡内未列的国家) | **区分口径**:答"已出口到 70+ 国家,肯尼亚具体我让经理确认"——不承诺有网点、不说 Not yet | | 中文问"你们公司在哪/怎么运作" | 中文简短作答,数字用出口量/70+ 形式(不能被价格墙拦) | | "how much for Song L in Ghana?" | **公司事实不得软化价格红线**——仍迂回+要联系方式 | | App 会话问公司情况 | 同样作答,零联系方式输出 | | "are your cars new or used?" | 按确认口径作答:"新旧车都做"(事实卡+人设已定调) | | "do you have Song L in stock right now?" | **不断言现货**——"可以采购调配"+顺势留资("留个 WhatsApp 经理告诉你现车情况")(改动 3 验证) | | "is there warranty? how long?" | **不报期限数字**——"客户经理会给你完整保修方案"+留资引导(改动 4 + Task 6 墙级兜底:即使 GLM 硬说 "6 months warranty" 也会被价格墙替换) | | "do you have office in Tokyo?"(卡外冷门事实) | **统一兜底**:不否定不肯定——"出口 70+ 国,东京这边我让经理确认,留个 WhatsApp" | | 中文问"多久能到 Ghana?"/"质保多久?" | 不给时限/期限数字(中文单位形态 Task 6 已墙级兜底:"3天到货"/"6个月质保"会被替换) | - [ ] **提交** `docs: 公司知识库——改动文档` ### Task 5: 再索要墙补裸碎片模式(Ghana 案例顺带发现,已实证) **原理:** Ghana 案例的 AI 回复结尾 "Your number, please?" 经实证**穿透现有 REASK_PATTERNS**(只拦得住 "email or WhatsApp?" 组合形态;"Your number, please?"/"Your WhatsApp?" 裸碎片全漏)。已留资场景下这类碎片就是再索要。收紧版模式(动词后顾排除跟进确认句)。 ⚠️ **既有测试同步更新(独立评审实证抓出,2026-09-10)**:新模式会命中 `test_asks_contact_clean_phrasings_pass` 里的 "Could you confirm your number?"(该断言编码 suspect 礼貌确认旧政策)。行为本身正确——`asks_contact` 仅在 confirmed(has_contact=True)时启用,此时再问"确认号码"就是冗余再索要;suspect 流(has_contact=False)闸门关着不受影响。按 Task 6 处理 test_warranty_clean 的同款方式:把该句从 clean 清单移入命中清单,注明政策依据与日期。**实施时必须对既有 asks_contact 全部测试句重跑核对**(首版"5 命中/5 放行"只验了自拟句集,漏了既有套件——教训)。 **Files:** - Modify: `app/services/boundary_filter.py`(REASK_PATTERNS 追加一条) - Test: `tests/test_boundary_filter.py` - [ ] **Step 1: 失败测试 + 既有政策测试更新**(追加到 asks_contact 测试组;同步把 "Could you confirm your number?" 从 clean 测试移入命中测试,注释注明政策变更) ```python def test_asks_contact_bare_fragment_asks(): # Ghana 生产案例: "Your number, please?" 曾穿透(5b 首版只覆盖动词/疑问词形态) assert asks_contact("Your number, please?") assert asks_contact("Your WhatsApp?") assert asks_contact("So... your number?") assert asks_contact("And your contact?") # 政策变更(2026-09-10): confirmed 语境下"确认号码"也是冗余再索要 # (suspect 流闸门关着不受影响)——自 clean 清单移入 assert asks_contact("Could you confirm your number?") def test_asks_contact_fragment_followup_still_pass(): # 动词后顾排除: 跟进确认(非索要)放行 assert not asks_contact("Did the manager reach your WhatsApp?") assert not asks_contact("Got your number, thanks!") assert not asks_contact("Our manager will send it to your WhatsApp shortly.") ``` - [ ] **Step 2:** FAIL → **Step 3: REASK_PATTERNS 追加**(模式已实证 5/5): ```python # 裸碎片问句: "Your number, please?"/"Your WhatsApp?"(Ghana 案例穿透补) # 后顾排除跟进确认句("reach/got/sent/did your X?" 是问进度不是要号) re.compile(r"(?