去年底我在重构一个传统文化内容项目时,遇到一个特别现实的矛盾:我们把《道德经》全文清洗、切句、向量化,再接入大模型做问答,本以为这就是「经典数字化」的终点。结果真实用户丢来一句话:"项目延期了,我该怎么跟团队交代?"模型回了一句「夫唯不争,故天下莫能与之争」,用户看完更焦虑了。
问题不在大模型,而在架构。我们把经典文本当成一个巨大的知识库去「问」,却没有回答「这个道理在什么场景下、以什么方式、讲给谁听」这三个前置问题。所以我后来把底层能力整个推翻重做,做成了现在的「道德经场景引擎」,并且在 v1.3 版本把接入方式收敛为一套标准化的快速集成方案。这篇文章就是这套方案的完整拆解。如果你正在做知识类产品、大模型应用,或者有把传统经典内容产品化的打算,这篇分享应该能帮你少走不少弯路。
1. 为什么是「场景引擎」而不是「聊天机器人」
先说清楚一个很容易被忽略的问题:为什么不能继续用聊天机器人那套架构来承载经典内容。
1.1 经典问答系统的两个天然缺陷
第一个缺陷是同一句话在上下文里可以被解释成完全相反的意思。比如《道德经》里「信言不美,美言不信」,单独拎出来,它既可以被解释成「好话都不好听,好听的话都不可信」,也可以被解释成「真实的东西不需要华丽修饰」。用户问一个具体问题,模型随机抽中哪种解释,完全不可控。这就是「原文可记忆、语义不可控」的典型表现。
第二个缺陷是风格不可控。你用一个通用聊天机器人的 system prompt 去约束它,它大概率会把回答写得像一篇议论文:先引用、再展开、最后升华。但产品里需要的往往是一段可执行的建议、一句能安抚情绪的话、一段符合场景语气的转述,这些目标差异很大。聊天机器人的答案是「即兴发挥」,而产品需要的答案是「有剧本的稳定输出」。
场景引擎的思路完全不同。它把「场景」当作第一公民:每一种使用场景都有明确的角色设定、知识注入约束、输出格式约定。大白话说,它像一个有剧本的演员,而不是一个临场发挥的脱口秀选手。
1.2 经典文本数字化的三个实际难点
既然要做场景化,就必须先正视《道德经》这类文本在数字化过程中的特有问题,我总结为三条:
| 难点 | 典型表现 | 引擎的处理方式 |
|---|---|---|
| 歧义性 | 一句原文在不同上下文中含义相反 | 场景模板先限定「立场」,检索结果只做佐证 |
| 粒度不均 | 有些章是长篇论述,有些章只有一句格言 | 以「句」为检索粒度,命中后附带整段作为上下文 |
| 典故与对仗 | 本身存在互文关系,单独切分语义残缺 | 用「证据窗」把原文前文、白话释义一起送入生成层 |
这里重点说一下「歧义性」。我在最早一版里吃过亏:同一句「无为而无不为」,用在「创业者该不该激进扩张」这个场景下,系统默认理解为「什么都不做」,导致给出了一段劝用户躺平的建议。后来加了场景模板后,问题就变成了「在创业决策场景下,无为是指不违背规律地行动,而不是不行动」——场景本身成了消歧的锚点。
1.3 引擎的定位:大模型之上的编排层
需要明确一点:道德经场景引擎不是一个大模型,它是跑在大模型之上的一层编排服务。它的职责有且只有四块:
- 理解场景配置(角色、目标、边界、知识权重)
- 把用户输入转化为检索条件,召回最相关的原文证据
- 按固定模板组装上下文,交给大模型生成
- 校验生成结果是否符合场景约束,必要时重试
想清楚这个定位后,后面所有设计就都顺了。引擎不需要自己会「思考」,它只负责让大模型在正确的位置、用正确的方式「思考」。
2. 从 v1.1 到 v1.3:三个版本主要改了什么
了解演进过程,比直接看最终版更有价值。每一版都不是为了加功能而加功能,而是被真实问题逼出来的。
2.1 v1.1:干净的检索问答,却败给了「问题不标准」
v1.1 架构很简单:原文清洗后按段落切分,用中文向量模型做嵌入,用户问题来了先向量检索 Top K,再把原文拼进 prompt 丢给大模型。
这版跑起来后很快发现两个问题:
- 用户提问方式极其不稳定。「怎么看待竞争」和「行业里碰到恶意竞争怎么办」检索出来的原文完全不同,前者老是召回「夫唯不争」这类抽象句,后者反而能召回一些实际可用的章节。
- 回答风格完全看模型心情。同一道题连续问五次,五次的长短、语气、引用密度都不一样,产品侧没法做展示。
2.2 v1.2:把「场景模板」加进来
v1.2 的核心变化是引入场景注册表。每个场景对应一份配置,里面预设了系统提示词、输入字段、输出边界。引擎侧开始区分「场景」和「会话」两个概念:场景是预设的剧本,会话是用户和剧本之间的实时互动过程。
这一版解决了风格漂移的问题,但暴露了新痛点:
- 场景配置写死在引擎侧代码里,接入方想微调语气,得提工单排队改配置。
- 没有引用溯源,用户问「你凭什么这么说」,系统答不上来。
- 多轮对话的历史消息会污染检索结果,越聊越跑偏。
2.3 v1.3:面向集成者的一次收敛
v1.3 的定位很明确:让外部接入方在不改引擎代码的情况下,通过配置和 SDK 就能跑通自己的场景。关键变化我用一张表说明:
| 能力项 | v1.1 | v1.2 | v1.3 |
|---|---|---|---|
| 场景配置 | 无 | 写死在引擎内 | 支持本地 YAML / 远程 JSON / SDK 内联字典 |
| 知识注入 | 固定 Top K | 固定 Top K | 可配置 knowledge_weight,支持按场景调节 |
| 接口形式 | 单问答接口 | 场景+会话接口 | 统一为 /v1.3 下的标准 REST 接口 |
| 引用溯源 | 无 | 无 | 返回 provenance 字段,含原文位置和置信度 |
| 调试模式 | 无 | 无 | 提供 dry_run,不消耗大模型 token 即可查看组装后的 prompt |
| 输出格式 | 纯文本 | 纯文本 | text / markdown / json / actions 四种模式 |
| 错误处理 | 简单超时 | 简单超时 | 标准化错误码 + 接入方熔断建议 |
这版最核心的设计目标,就是让「把《道德经》接进自己的产品」这件事,从一个月变成一两天。
3. 引擎四层架构与运行细节
这一章会按知识层、场景层、生成层、服务层的顺序拆开讲,每一层都能落地。
3.1 知识层:句子级切分比段落级更可靠
先说数据处理。原始文本经过清洗后(去掉注释、标点统一、保留原文),我建议优先做句子级切分,而不是段落级。原因很简单:用户问「怎么看待刚柔关系」时,如果能直接命中「天下之至柔,驰骋天下之至坚」这句话,再向上附带整章作为上下文,输出质量会明显更好。如果按段落切,一段几百年流传的论述往往包含多个语义点,检索出来容易「面上沾边、点到即止」。
每个切分单元需要维护的 metadata 字段如下:
| 字段 | 含义 | 示例 |
|---|---|---|
| chapter_id | 所属章 | 43 |
| sentence_id | 句级编号 | 43-03 |
| text | 原文片段 | 天下之至柔,驰骋天下之至坚 |
| is_core | 是否核心金句 | true |
| plain_text | 白话释义 | 天下最柔弱的东西,能驾驭天下最坚硬的东西 |
切分完成后用中文向量模型生成句子向量,建议维度 1024(版本不同会有差异,只要向量库兼容即可)。至于选哪个向量库,我的建议是:选一个支持 1024 维向量、能按 metadata 过滤的开源库就行,不必刻意追新。这个小项目优先考虑的是稳定和可导出。
3.2 场景层:一份 YAML 就是一套行为规范
场景层是整个引擎的灵魂。一份标准的场景配置长这样:
scene_id: workplace_decision name: 职场决策安抚 description: 面向职场决策咨询,以道家视角给出稳定、平和的建议 version: "1.0" system_prompt: | 你是深谙道家智慧的职场顾问。 你的原则: 1. 先认可对方的处境,再给出策略。 2. 所有建议必须落回现实行动,不能只说玄理。 3. 除明确标注外,不主动引用原文。 4. 语气平和,不使用排比堆砌。 knowledge_weight: 0.6 # 检索知识的强度,0 为不注入 input_format: - issue_type # 问题类型 - description # 用户对具体事情的描述 constraints: max_quote: 1 # 最多引用一处原文 output_mode: text # 输出为文本 language: 白话为主 # 禁止文言堆砌写 system_prompt 有三个原则,是我多次踩坑后总结的:
- 先定义角色,再给边界。角色决定了语气,边界决定了「它绝对不能做什么」。上面例子里「不主动引用原文」就是边界。
- 明确引用规则。是「最多引用一次」还是「必须引用」,场景里写死,模型就不会自由发挥。
- 写「不要做什么」比写「要做什么」更有效。比如「不能只说玄理」「不使用排比堆砌」,直接命中大模型的典型毛病。
knowledge_weight是个 0 到 1 的浮点数。0 表示完全不注入原文,适合纯粹讨论心法;0.6 表示在注入原文和自由发挥之间取平衡。我个人测试下来的体感是:偏实用的场景设 0.4 到 0.6,偏文化解读的场景设 0.8 以上。权重过高时,回答容易被原文牵着走,显得文绉绉。
3.3 生成层:上下文组装和输出校验
生成层做的事情,是把「场景配置 + 用户输入 + 检索证据」按固定模板拼成最终请求。组装顺序我建议固定为:
- 系统提示词(来自场景配置)
- 用户的问题描述
- 检索到的原文证据(带出处)
- 输出约束说明(格式、字数、引用规则)
这个顺序很重要。如果把检索证据放在用户问题之前,模型容易把「证据」当成指令;放在之后,证据才更像「参考资料」。顺序影响注意力分配,这是大模型上下文机制里非常微妙的地方。
参数方面,我给一组实测后比较稳的默认值:
| 参数 | 推荐值 | 说明 |
|---|---|---|
| temperature | 0.3 | 太高容易自由发挥,引用失真 |
| top_p | 0.9 | 与 temperature 配合控制多样性 |
| max_tokens | 800 | 场景回答不需要长篇大论 |
| timeout | 10s | 见下文「最容易被忽略的配置」 |
生成完成后还有一道校验。校验分两层:一是格式校验(是否按output_mode输出),二是引用校验(如果要求引用原文,则检查是否存在并标注出处)。校验不通过时,引擎会用更严格的一次提示词重试;如果重试仍失败,才返回错误。
3.4 服务层:对外接口与错误码设计
服务层我用 Python 生态里一个轻量异步 Web 框架包了一层 REST API,核心接口只有三个:
| 方法 | 路径 | 作用 |
|---|---|---|
| POST | /v1.3/scenes/{scene_id}/sessions | 创建会话 |
| POST | /v1.3/sessions/{session_id}/messages | 发送消息,获得回答 |
| GET | /v1.3/health | 健康检查 |
错误码设计上,我坚持了两个原则:区分接入方错误和引擎侧错误。4001 表示场景不存在(接入方配置问题),5001 表示模型超时(引擎侧或上游问题)。接入方拿到错误码后可以直接做分类处理,而不是统一当成「接口挂了」。
限流策略很简单:单 IP 每自然分钟最多 30 次请求。超过会返回 429。这个阈值是我们做了压测后定的,既能保证稳定性,又不会让正常的批量接入受阻。
4. v1.3 快速集成:从安装到跑通第一个场景
下面进入正题,整个过程按正常手速操作,20 分钟内能跑完。
4.1 安装与初始化
先装 SDK:
pip install md-scene-sdkSDK 同时提供同步和异步接口,如果你自己的服务也是基于异步框架写的,可以直接用async版本,免得再多做一层线程池包装。
初始化客户端:
from md_scene import SceneClient client = SceneClient( api_key="your_api_key", base_url="https://api.example.com/v1.3", default_timeout=10, # 见后面的超时说明 )初始化后先检查一下版本,这里特别建议做。因为 v1.3 和 v1.2 的接口路径有调整,如果服务端还没升级到 v1.3,调用时容易撞上 4001:
info = client.get_version() assert info["version"].startswith("1.3"), "服务端版本过旧,请升级"4.2 跑通第一个场景:职场决策
假设你已经配置好了workplace_decision场景(没配好的话,SDK 支持直接传一个 dict 作为场景配置,后面会讲)。调用逻辑分两步:先创建会话,再发消息。
# 第一步:创建会话 session = client.create_session( scene_id="workplace_decision", user_id="u_001", ) # 第二步:发送消息 resp = client.send_message( session_id=session.session_id, message="我带的项目组最近有两个晋升名额,但团队里好几个骨干都觉得该是自己,我现在不知道怎么处理。", ) print(resp.answer)实际返回的answer大概是这样的风格:
名额只有一个,这是事实。不用急着马上安抚所有人,先判断两件事:一是哪些人真的在意结果,二是哪些人只是需要被看到。你可以先单独沟通一轮,把晋升标准讲清楚,「知人者智,自知者明」,让他们把注意力放回可改善的地方,而不是放在名额上。
这版输出已经不会再出现大段文言堆砌,原因是场景配置里的constraints.max_quote: 1和「禁止排比堆砌」起了作用。
resp.provenance里会带回引用的来源,方便你做数据展示或二次核验:
print(resp.provenance) # [{"quote": "知人者智,自知者明", "chapter": 33, "confidence": 0.91}]4.3 调试利器:dry_run 和 JSON 输出
集成阶段我最推荐你先用dry_run模式。它不调用大模型,而是直接返回「如果发给大模型,请求体长什么样」。这样你能快速确认场景配置和用户输入合在一起后,prompt 是否符合预期。
preview = client.dry_run( scene_id="workplace_decision", message="项目延期了,我该怎么跟团队沟通?", ) print(preview["assembled_prompt"])dry_run 不消耗 token,非常适合写进自动化测试里当回归用例。改场景配置后跑一圈 dry_run,能第一时间发现 prompt 拼装异常。
如果你的产品需要结构化数据,把output_mode设为json:
resp = client.send_message( session_id=session.session_id, message="...", output_mode="json", ) print(resp.answer_json) # {"summary": "...", "action_items": ["...", "..."], "quote": "..."}4.4 最容易被忽略的三个接入配置
第一,超时时间千万别用默认值。这个引擎的链路是「检索 + 生成」,平均耗时 2.8 秒左右,但长尾请求会超过 5 秒。如果超时设得太短,你会发现线上时不时冒出「504」或「timeout」,而这其实不是服务端挂了,是客户端等得不够久。建议显式设为 10 秒,重试策略用指数退避。
第二,temperature 必须显式传入。我见过不止一个接入方直接不传,结果用大模型服务的默认值,整体回答风格变得非常放飞。配合场景配置里的引用规则,0.3 左右才稳。
第三,正式环境前先配好场景版本号。场景配置如果有version字段,SDK 会在每次请求时带上;服务端收到后如果在做配置灰度,可以保证旧会话不突然变风格。这是个「早配早省心」的字段。
5. 接入产品时踩过的坑与解法
这个章节的内容来自我和几个合作方接入时的真实经历,每一条都对应过线上事故或明显的效果劣化。
5.1 「文言输出病」:提示词里决不能夸古文
有一个合作方接入时,为了体现「专业感」,在 system_prompt 里写了「请用古朴简练的语言回答」。结果线上输出变成这样:
功成弗居,众望所归。祸福相依,顺其自然。少私寡欲,知足常乐。
整段全是四字句排比,看起来很有文化,实际上一点信息量都没有。用户看完只会觉得「你说得对但我没法用」。
诊断后发现:古文语料对大模型有很强的诱导性,它一旦进入「仿古模式」,就很难主动切回白话。修复办法是在场景配置里明确加一条:
constraints: language: 白话为主 forbid_parallelism: true # 禁止排比堆砌同时在 system_prompt 里写死一句:「除必要引用外,一律使用现代白话,禁止出现四字成语连续堆叠。」改完之后,回答的可读性立马上了一个台阶。
5.2 引用溯源会「记错章节」
v1.3 刚上线时,我们收到过一次线上反馈:系统引用「上善若水」,后面标注的出处却是「第二十二章第九句」。查证后发现正确出处应该是「第八章第七句」。
排查链路是这样的:
- 先看日志里的 citation 元数据,发现大模型返回的引用文本是对的,但章节号错了。
- 再看知识检索环节,发现向量检索召回的确实是「上善若水」所在的句子,但置信度不高,约 0.76。
- 进一步定位后确认,是成语类短文本的向量区分度太低,「上善若水」和「上德若谷」这类句子的向量距离过近,导致系统偶尔把上下文归属搞混。
解法分两层。第一层,检索召回时设一个置信度阈值,低于 0.85 的召回结果宁可不返回引用,也不返回可能出错的引用。第二层,做了一张「金句表」,把用户最常问的 500 条核心名句单独建表,走精确匹配优先;只有金句表没命中时才走向量检索。这个组合方案上线后,引用章节错误的投诉基本清零。
5.3 多轮会话的历史污染
接入方做多轮对话时最容易踩的坑是:用户上一轮提到「我们公司最近在裁员」,下一轮问「那我们还要不要启动新项目」,引擎会把「公司」「裁员」等词也当成检索条件,导致召回一些完全不相关的原文。
v1.3 在 session 上提供了context_cleaner参数,可以过滤历史消息里的组织机构名、人名和高频噪音词。另外,SDK 端也建议主动控制历史窗口:
session = client.create_session( scene_id="workplace_decision", user_id="u_001", history_window=6, # 只保留最近 6 轮历史 context_cleaner=True, # 清洗掉噪声实体 )如果业务场景本身不需要上下文,最简单粗暴的方式是每次请求都创建新 session,只传当前问题。这能规避 80% 的多轮污染问题。
5.4 并发一上来,超时率就飙升
我们做过一次压测:单引擎实例从 10 QPS 逐步加到 30 QPS,超时率从 0.5% 涨到 8%。看起来是引擎扛不住,但打开日志一看,真正的问题是大模型上游的响应变慢了,平均耗时从 2.8 秒拉长到 7 秒多。
这里有两个教训:
- 接入方要做客户端熔断。连续 3 次超时后,切换到一个轻量兜底场景(只做白话释义,不检索知识),避免用户长时间等待。
- 引擎侧要对高优先级场景做队列隔离。内部把「实时对话」和「批量生成」两条流量分开,批量任务再大也不挤占实时通道。
调整后,30 QPS 下的超时率降到 2% 以内,属于可接受范围。如果要求更高,就得考虑垂直扩容了。
5.5 场景配置的版本化管理
最后一个建议和代码无关,但我觉得比任何一行代码都重要:把每个场景配置当代码一样管理。
我现在的项目目录结构是这样的:
scenes/ workplace_decision/ config.yaml test_cases.md changelog.md life_anxiety/ config.yaml test_cases.md changelog.md每个场景目录下,除了config.yaml配置,还要维护一份test_cases.md(手写的典型提问与预期回答)和一份changelog.md(改了哪个字段、为什么改、影响范围)。这让你三个月后回头看时,还记得当初为什么把knowledge_weight从 0.5 改成 0.6。没有这份记录,场景配置就是一堆没有记忆的 YAML。
6. 从场景引擎到行为教练:后续可以怎么扩展
v1.3 的「快速集成」只是起点。我自己的使用场景已经不止于问答,往下来还有三个方向可以延伸:
方向一:把回答转成行动清单。引擎支持output_mode="actions",会在回答之外额外产出一组结构化建议动作。比如前面那个职场晋升的场景,会生成「先分别与三位候选人沟通」「在周会上公布明确考核标准」「两周后做一次复盘」这样的行动项。这让引擎从「讲道理」变成了「给下一步」。
方向二:多场景编排。场景引擎的每个场景解决单一问题,但产品流程往往是多段的。比如先让用户描述处境(信息收集场景),再给决策建议(决策安抚场景),最后复盘执行效果(复盘场景)。这些场景可以全部接入一个产品流程里,通过你自己的业务流程编排串联起来,引擎只负责每个节点上的专业输出。
方向三:场景质量回归。我给每个场景都建了一个 50 到 100 条的小型评测集,每周跑一次回归。主要盯两个指标:输出空回答率(该给建议时给了玄学,或者干脆没输出)和引用置信度(引用章节是否正确)。一旦这两个指标出现劣化趋势,立即回溯是上游模型变了,还是场景配置被误改了。这套回归机制,才是场景长期稳定运行的安全带。
我在实际使用中最深的一点体会是:经典文本进入产品,真正有价值的地方在于「场景化转述」,而不是「原文复读」。用户要的不是一句「道可道非常道」,是这句话在当下这个局面里到底意味着什么、他能做什么。场景引擎解决的核心问题,其实就是把这个「转述」过程变得稳定、可配置、可复用。
最后分享一个做集成的小技巧:从一个最小场景开始。不要一上来就规划十个场景,先拿一个真实用户高频问题,配置一个场景,跑通,建好评测集,再复制到其他场景。一个场景、一份配置、一套测试用例,这样滚动的节奏,比「大而全」稳得多。