【电商小二】详细开发总流程
本文按笔记 01~11 的开发顺序整理,只讲流程、设计决策与验证方法,不含具体代码。
回答三个问题:每一步做什么?为什么这一步要做这个?做完怎么验证它是对的?
需要代码时回到对应章节笔记(01~11)。
0. 开发总流程一图流
flowchart LR
subgraph 搭骨架
P0["阶段0 准备<br/>项目+环境+依赖"] --> P1["阶段1 领域模型<br/>先定数据形状"]
P1 --> P2["阶段2 流程定义<br/>YAML 可配置化"]
P2 --> P3["阶段3 三层骨架<br/>HTTP→DB 链路打通"]
end
subgraph 填主干
P3 --> P4["阶段4 真引擎+LLM规划<br/>文本→TurnPlan→task入口"]
P4 --> P5["阶段5 防幻觉+对象消息<br/>安全网+点卡片"]
P5 --> P6["阶段6 命令执行<br/>任务生命周期"]
P6 --> P7["阶段7 动作体系<br/>查接口/发消息"]
P7 --> P8["阶段8 流程执行器<br/>Task轨道闭环"]
end
subgraph 补全
P8 --> P9["阶段9 知识+闲聊轨道<br/>三轨道完整"]
P9 --> P10["阶段10 历史对话<br/>刷新可见历史"]
end
贯穿全程的开发节奏(方法论):
| 节奏 | 含义 | 体现 |
| 从外到内 | 先搭好"管道"(Web/Service/Repo),再往里填逻辑 | 阶段 3 先于阶段 4 |
| 先占位、再替换 | 假实现打通链路,契约(方法签名)不变再换真货 | 占位引擎 → 真引擎 |
| 先主干、再加固 | 正常路径先跑通,异常路径防护后补 | 校验在阶段 5 才做 |
| 每阶段可验证 | 每个阶段结束都有"能跑、能看"的成果 | 见各阶段"验证" |
| 配置优先于代码 | 业务行为写在 YAML,不写死代码 | 阶段 2 起全项目贯彻 |
| fail fast | 配置错误尽早暴露,别等到运行期 | 加载期槽位重复检测 |
阶段 0:准备
做什么
- 还原两个数据库脚本(客服库 + 电商库)
- 启动模拟电商后端(
ecommerce-service-backend,提供订单/物流/商品接口)
- 启动前端页面(
customer-service-frontend,教学可视化控制台)
- 创建核心工程
customer-service-backend:目录结构、.env 配置、pyproject.toml、安装依赖
- 搭好三个基础设施:LLM 客户端(LangChain 统一封装,换厂商不换代码)、HTTP 客户端(httpx 异步全局单例)、数据库引擎(SQLAlchemy 异步 + 连接池)
为什么先做这一步
- 先把"外部世界"准备好(数据库、电商接口、前端、LLM),后面的开发才有验证对象
- 基础设施是全局单例,整个项目所有层都在用,必须最先立起来
- 异步贯穿始终(FastAPI 是 async 的),所以数据库、HTTP、LLM 调用全部用异步方案
关键决策
| 决策 | 原因 |
| 用 uv 管理依赖 | 声明式依赖管理,uv sync 一键安装 |
expire_on_commit=False | 异步场景下 commit 后对象不会被标记过期、不触发隐式查询 |
配置全部走 .env | 环境相关的(LLM 地址/key、数据库 URL、端口)与代码分离 |
@lru_cache 缓存配置 | 配置只读一次,全局共享 |
验证
- 前端页面打开、能显示订单/商品列表
- LLM 客户端
invoke("你好") 能返回
- 数据库
SELECT 1 能连上
阶段 1:领域模型 —— 先定"数据的形状"
做什么(三个文件,由小到大)
- 消息模型:用户消息、机器人消息、聚焦对象(订单/商品卡片)、消息类型、引擎返回值
- 任务上下文模型:业务任务快照 + 系统过场快照(基类 + 5 个子类 + 反序列化适配器)
- 对话状态聚合根:把消息、任务、系统过场、聚焦对象、历史会话全部装进一个
DialogueState,并实现全套操作方法
为什么先做这一步
先定义"这个世界里有什么东西、长什么样",再谈"怎么处理"。 后续所有层(引擎、处理器、执行器)都在这套数据结构上工作,它错了后面全错。
每个模型的为什么(推导链)
| 模型 | 解决什么问题 |
FocusedObject | 用户点了一张订单/商品卡片,后续对话都"与它相关"——需要一个对象承载它 |
MessageType(TEXT/OBJECT) | 两种输入形态:打字 vs 点卡片,处理方式完全不同,需要标记 |
TaskContext | 多任务对话:用户同时在办几件事,需要区分"正在做的"和"挂起的",记录做到哪一步、收集了什么 |
SystemContext(5 子类) | 任务切换时的"过场白"(开始/打断/取消/恢复/补槽)不属于任何业务流程,必须单独建模,否则多任务切换说不清 |
| 区分联合反序列化 | 5 个子类要从 JSON 还原,需要一个能自动识别子类的序列化方案(Literal 标签 + 适配器) |
DialogueState 聚合根 | 一次真实对话要记的东西太多(任务/挂起栈/过场/聚焦对象/历史),需要一个"中央仓库"统一管理 |
pending_turn 两步提交 | 处理中途出错时,历史里不能留"半成品"轮次 → 先暂存、后落盘 |
| 整份 JSON 存储 | 状态总是"整份读、整份写",从不按内部字段查 → 一张表两列最合适(DDD 聚合根思想) |
关键方法组(只列职责,不列代码)
- 任务组:开始任务 / 结束任务 / 取消(丢弃)/ 打断(挂起入栈)/ 恢复(弹出栈,支持指名与不指名)
- 过场组:激活/结束系统过场;"当前任务"返回时系统过场优先
- 槽位组:合并写入 / 单个移除
- 会话组:取当前会话 / 开新会话 / 关会话 / 超时重置(清运行时状态但保留历史)
- 轮次组:begin(暂存)/ commit(落盘)
验证
- 每个模型文件用
if __name__ == '__main__' 跑自测:构造对象 → 序列化成字典 → 反序列化还原 → 打印确认
- 重点测:5 个子类能否自动识别还原;
DialogueState 各方法在不同对话场景下的状态变化
阶段 2:流程定义与加载 —— 让业务"可配置化"
做什么
- 学习 YAML 语法(缩进、键值对、列表、嵌套、多行文本、安全加载)
- 定义流程数据模型:链接(静态/条件/兜底)、步骤(start/end/action/collect)、容器(流程 + 全局槽位)
- 写加载器:把
user_flows.yml + system_flows.yml 读进来,转成内存对象
为什么做这一步
- 业务流程(步骤、提示语、跳转)不该写在代码里——写代码里加一个流程就要发一次版;写 YAML 里加一份文件就完事
- 把流程抽象成"有向图":节点 = 步骤,边 = 链接,加载器把 YAML 变成图
关键设计决策
| 决策 | 为什么 |
| 4 种 step 类型 | 覆盖流程的所有形态:起点、终点、执行动作、收集信息 |
| collect 是语法糖 | 收集一个槽位本质是"问一句 + 等一句",若每个槽位都写两个 step 会爆炸 → 收敛成一个 collect,复用系统过场 system_collect_information 完成"问+等" |
| 3 种链接 | 流程不止顺序走,还要支持条件分支(有商品就推荐、没有就引导)→ 静态/条件/兜底 |
| 全局 slots 集中声明 | 槽位元信息(类型、标签、描述)供 LLM 理解流程用;流程只按名引用;加载时自动挑出每个流程用到的槽位 |
| 加载期查重(fail fast) | 两个 YAML 文件若有同名槽位,启动即报错,而不是运行期悄悄覆盖 |
| 步骤按 type 多态分发 | YAML 里写 type: start,加载成对应的步骤子类——"字符串 → 类"映射表 |
yaml.safe_load | 防止 YAML 里藏可执行代码(安全) |
验证
- 加载器跑通:两个 YAML → 打印出
FlowsList,能看到每个流程的名称、步骤数、用到的槽位
- 手动构造一个"坏了"的配置(重复槽位),确认加载期报错
阶段 3:三层骨架 —— 让链路先"通"起来
做什么
- 两套模型分工:接口模型(给前端看的契约)vs 领域模型(内部业务),Web 层负责翻译
- ORM 表映射:一张表、两列(用户 ID 主键 + 整份状态 JSON)
- Repository 层:读取状态(查不到就返回全新空状态,上层不用区分新老用户)/ 保存状态(upsert,一条语句搞定插入或更新)
- 占位引擎:不智能,只回固定话,但方法签名和真引擎完全一致
- Service 层:三步编排——加载状态 → 调引擎(引擎就地改状态)→ 保存状态
- Web 层:路由 + 依赖注入(FastAPI Depends 组装整棵对象树)+ 应用生命周期(启动时初始化数据库连接池、关闭时释放)
为什么先搭骨架、引擎先占位
引擎是最复杂的部分(LLM 路由、流程推进、知识检索)。如果等它写完再搭 Web,周期太长,也没法验证。
先用最小占位实现把整条链路(HTTP → Service → Repository → DB → 返回)当场跑通;下一阶段换真引擎时,上游一行代码不用改——这就是"面向接口编程":只要签名(契约)不变,实现随便换。
关键设计决策
| 决策 | 为什么 |
| 两套模型 | 接口契约与内部业务解耦,内部重构不破坏对外契约 |
| 整存 JSON + upsert | 每次对话"整份读、整份写",拆多张表纯属自找麻烦;upsert 一次往返搞定,还避免并发竞态 |
| I/O 在两端、计算在中间 | 只有 Service 碰数据库;引擎是纯内存计算、无副作用、易测试;事务边界清晰 |
| 依赖注入 | 路由不自己 new 对象,由框架按依赖链组装(session → repository → service),生命周期自动管理 |
| 新用户给空状态 | 上层逻辑不用判断"老用户/新用户",拿到的永远是可用的状态 |
验证(里程碑 ⭐ 骨架通)
POST /api/chat 发一条 {"sender_id": "u1", "text": "你好"}
- 数据库
dialogue_states 表能看到 u1 这行、状态 JSON 已写入
- 再发一条,能看到状态被覆盖更新
- 链路真实可运行:HTTP → 数据库 → HTTP 全通
阶段 4:真引擎 + LLM 规划 —— 让"理解"发生
做什么
- 真引擎五步框架:准备会话 → 开启本轮 → 按消息类型分流 → 回复写入并提交 → 组装返回
- 会话管理:无会话开新;超过 60 分钟无活动→ 关旧会话、重置运行时状态(清任务/挂起/聚焦对象)、开新会话;未超时则续期
- 规划产物数据模型:
TurnPlan(三个轨道字段)+ Command(4 种原子指令)
- TurnPlanner:把对话状态组装成 7 个 prompt 字段 → 渲染 Jinja2 模板 → LLM 调用链(模板 | 模型 | JSON 解析)→ 解析成 TurnPlan
- HistoryBuilder:把历史对话渲染成文本喂给 LLM(对象消息渲染成
[订单对象 id=..., title=...] 描述)
为什么这样设计
| 决策 | 为什么 |
| 引擎只调度、不干活 | 引擎持有一堆"专员"(planner/各类 handler),自己只决定走哪条轨道、派活给谁——职责清晰、易扩展 |
| LLM 输出结构化 JSON | 不直接让 LLM 自由发挥,而是让它填 task/knowledge/chitchat 三选一 + 结构化指令,代码才能可靠执行 |
| 4 种 Command | 回顾用户办业务的所有动作,就四类:发起 / 补信息 / 放弃 / 回头继续——"自然语言动作 → 原子指令" |
| 7 个 prompt 字段 | 分三组:判断输入(这句话、最近 10 轮历史)、当前状态(活跃任务、挂起任务、聚焦对象)、可选范围(可用流程清单、知识意图清单)。给 LLM 看什么,决定它判断得准不准;可选范围字段就是"给 LLM 划圈"防止瞎编 |
| 流程清单去掉 steps | LLM 只需要"业务说明书"(有哪些流程、干什么的),不需要内部步骤——省 token 又防干扰 |
| 先不做校验 | 这是"先主干、再加固"的刻意取舍:正常路径先通,防护留到下一阶段 |
验证(里程碑 ⭐ 主干通)
- 说"我要退款"→ LLM 返回
{"task": {"commands": [{"command": "start_flow", "flow": "refund_request"}]}} → 进入 task 轨道入口(内部是占位,先回固定话)
- 说"今天天气不错"→ 观察 LLM 是否走 chitchat(此阶段先不实现,能看到规划结果即可)
阶段 5:防幻觉 + 对象消息 —— 安全网与"另一只手"
做什么
- 校验器:检查 TurnPlan 能不能执行,不能就给一个"原因码"(9 种)
- 对象消息处理:点卡片的三场景处理 + 判断"对象能否填当前槽"
- 澄清回复器:原因码 → 基础话术 → LLM 润色 → 追问用户
- 知识意图注册表:定义 7 类知识意图(商品/订单/政策/规则…),为知识轨道做铺垫
为什么做这一步
- LLM 是概率模型,输出不保证合法:可能三个轨道全 null、可能编一个不存在的流程 ID、可能一次开两个流程——直接拿去执行会崩溃。上一阶段刻意跳过的"防护",现在补上
- 用户不只打字,还会点卡片(订单/商品)——这是完全不同的输入形态,此前一直没处理
关键设计决策
| 决策 | 为什么 |
| 9 种原因码分三组 | 轨道层(没判断出方向/多个方向)→ task 内部(命令为空/类型不合法/多个流程/流程不存在)→ knowledge/对象(意图为空/缺聚焦对象/只发对象没说要干嘛) |
| 校验只判断、不回复 | 校验器只输出 valid + reason,话术交给澄清回复器——职责单一,互不污染 |
| 多意图不强选 | 命中多条轨道时不替用户猜,触发澄清让用户决定先办哪个 |
| 最典型的防幻觉检查:流程必须真实存在 | LLM 可能把 refund_request 编成 refund 或 tuikuan——拿真实流程清单查证,查不到就是幻觉 |
| 对象消息不经过 LLM | 点一个订单的语义是明确的,不需要"理解",与文本消息本质不同 |
| 对象消息三场景 | 能填当前缺的槽→填槽推进;没流程→澄清意图;在流程但槽不匹配→不打断,让流程继续 |
| 对象归一化为命令 | 点卡片填槽也构造 SetSlotsCommand,与打字填槽走同一条链路,复用流程推进逻辑 |
| 澄清"规则保底 + LLM 润色" | 先按原因码写死一句兜底话术(保证永远有话说),再让 LLM 润色得更自然(保证说得好听) |
验证
- 故意让 LLM 输出不存在的流程 → 校验拦截,返回澄清追问
- 三场景点卡片:流程缺订单号时点订单(填槽推进)/ 无流程时点订单(澄清)/ 流程收退款原因时点订单(不打断)
- 澄清回复自然贴合上下文(带上了订单号等细节)
阶段 6:命令执行 —— 任务生命周期
做什么
- TaskHandler:两阶段协调者——先让命令处理器改状态,再让流程执行器推流程(后者下阶段才实现,先占位)
- CommandProcessor:把 4 种命令真正应用到状态上
关键设计决策
| 决策 | 为什么 |
| 命令逐条按序应用 | 一轮可能多条命令("我要退款,订单号 A001"→ start_flow + set_slots),顺序有意义:必须先建任务,槽位才有地方填 |
| start_flow 五种处境 | 有没有活跃任务 × 目标在不在挂起栈,决定:新建 / 从栈恢复 / 打断当前再新建 / 打断后恢复目标 / 重复启动直接忽略 |
| 改状态 + 激活过场成对出现 | 每次改任务状态都顺手激活一个系统过场(开始/打断/取消/恢复),让系统说出贴合处境的话——过场文案在 system_flows.yml 里 |
| resume 支持指名与不指名 | 指名按 flow_id 精确恢复;不指名("继续刚才的")恢复栈顶——LIFO 贴合"接着最近那件事"的对话直觉 |
| resume 失败回退 | 先打断当前任务、再尝试恢复目标,若目标不在栈中则把刚打断的任务恢复回来——"要么全做、要么撤销" |
| 取消 vs 打断 | 打断→进挂起栈可恢复;取消→直接丢弃。这是任务生命周期最需要分清的一对 |
| 空命令列表照常推进 | 对象消息"槽不匹配不打断"场景传空命令——命令处理器什么都不做,直接进入流程推进 |
验证(核心:连续对话推演)
跑"我要查订单状态 → 先帮我查物流 → A001 → 算了还是看订单状态吧",逐步观察 active_task 与 paused_tasks 像栈一样此起彼伏,系统过场话贴合每步切换。
阶段 7:动作体系 —— 让系统"干实事"
做什么
- 框架四件套:动作基类(名字 + 异步 run 方法)、动作结果(回复消息 / 槽位更新 二选一)、动作注册表(名字→实例)、动作执行器(查表→调用→返回)
- 内置动作:
action_response(三种回复模式)+ action_listen(哨兵,等用户输入)
- 自定义动作:查订单状态、查物流、推荐相似商品(占位)+ 调电商接口的共享工具
- 自动注册:内置手动注册;自定义目录启动时自动扫描注册
为什么做这一步
- 前面所有阶段都在改"状态",但没有任何代码真正干过活:没查过订单、没调过接口、没生成过回复文本。YAML 只编排"先做什么后做什么","具体怎么做"必须由 Python 实现——这就是 Action
关键设计决策
| 决策 | 为什么 |
| YAML 是骨架,Action 是血肉 | 编排与实现分离:加流程不用写代码,加能力不用改 YAML |
| Action 不直接改状态 | 只返回"回复消息 / 槽位更新",由调用方统一写回——副作用集中、可测试 |
| 查与说分离 | 查询类动作写槽(slot_updates),action_response 读槽拼话——两步搭配,"查到数据"和"组织语言"解耦 |
action_response 三模式 | static(模板直出,最常用)/ rephrase(底稿 + LLM 润色)/ generate(LLM 从零生成)——按"要不要 LLM、用 LLM 干什么"区分 |
| 优雅降级 | 接口调用 try/except 返回 None,动作写"查询失败"降级槽位——接口挂了流程也不崩 |
action_listen 哨兵 | 本身不干活,作为"该等用户输入了"的退出信号(下阶段执行器靠它跳出循环) |
| 自动发现 | 新增动作 = custom 目录下写一个类 + YAML 配置一行,注册代码零改动(开闭原则) |
验证
- 每个动作单独跑:查订单/物流动作返回正确槽位;失败场景返回降级槽位
- 自动注册验证:新增一个动作类,启动后注册表能查到
阶段 8:流程执行器 —— Task 轨道闭环
做什么
把之前所有"零件"(流程定义、状态、命令、动作)串起来跑:
- 两层循环:外层管动作执行,内层管步骤推进
- 四种 step 的处理:start(推进)/ end(清任务)/ collect(补槽判断)/ action(推进 + 构造调用请求)
- 条件跳转:表达式求值(受限 eval)+ 三种链接的选择
- 自动补槽:collect 步骤先检查聚焦对象能否直接提供槽位
关键设计决策
| 决策 | 为什么 |
| 两层循环 + action_listen 哨兵 | 内层沿 YAML 图推进到"下一个要执行的动作",外层执行动作、把槽位更新写回状态、收集回复;遇到哨兵就退出等用户——推进与执行是两种粒度,拆开避免每步都做类型判断 |
| 系统任务优先 | 当前任务返回时系统过场优先(一行 or 短路)——过场/补槽追问"覆盖"在业务流之上执行,不需要复杂的挂起恢复框架 |
| action 步骤先推进再执行 | 先改 step_id 再返回动作调用,否则执行完会停在同一个 action 上死循环 |
| collect 复用系统过场 | 激活补槽过场时把问句作为参数传入;过场内部用 context.response 间接引用取问句——一个系统流服务所有业务 collect 步骤 |
| 自动补槽 | 解决"用户先点卡片、后说意图"的跨轮场景:聚焦对象跨轮持久化,进 collect 步骤主动检查、直接填入,省掉一次追问 |
| 受限 eval | 条件表达式用 eval 执行,但屏蔽内置函数、只暴露 slots 和 context 两个变量——安全 |
验证(里程碑 ⭐ 核心闭环!)
- 完整跑通退款流程:"我要退款" → "好的,我们先处理退款申请" → "请告诉我你的订单号" → 用户给号 → "请简单说一下退款原因" → 用户给原因 → "退款申请已提交,原因是……"
- 查订单流程:先点订单卡片再查(验证自动补槽)
- 条件分支:推荐商品流程无商品时走引导分支
- 整个客服系统的核心逻辑——业务任务处理——至此实现完毕
阶段 9:补全知识 + 闲聊轨道 —— 三轨道完整
做什么
- 闲聊轨道(轻):取"用户消息 + 最近历史" → LLM 自由回复。没有流程、没有检索、没有命令
- 知识轨道(重):意图 → 知识来源 → 检索 → LLM 生成回复
- 意图注册表(上阶段已建)把"用户问什么类"映射到"查哪些知识源"
- 四种知识提供者:商品 API、订单 API(并发查订单+物流)、FAQ(占位)、RAG(占位)
- 提供者注册表:id → 实例
- 回复生成器:把检索到的知识块拼进 prompt,约束 LLM"只按已知信息作答、不编造"
关键设计决策
| 决策 | 为什么 |
| handler 不干活 | 两个 handler 都只取原料(用户消息、历史、意图),真正调 LLM 的是 responder——调度与实现分离 |
| provider 抽象(契约先行) | API / FAQ / RAG 用同一个"检索接口";FAQ/RAG 当前占位,将来接 ElasticSearch / 向量库只换检索实现,上层零改动 |
| 意图注册表三重角色 | 同时当 LLM 的分类词表(约束它只能从这些意图里选)、校验白名单、路由元数据 |
| OrderAPI 并发检索 | 一个意图要查订单+物流两个接口,用并发合并等待时间 |
| 知识回复三约束 | "不编造、信息不足坦诚告知、语气自然不机械复述"——知识问答质量的底线 |
验证(里程碑 ⭐ 全功能闭环)
- 问"退款政策"→ 走 knowledge 轨道返回政策回答
- 点商品卡片后问"这个商品什么情况"→ 查商品 API 后按知识回答
- 说"你好"→ 走 chitchat 自然打招呼
- 三轨道互斥验证:互斥逻辑不冲突
阶段 10:历史对话 —— 刷新可见历史
做什么
- 历史接口:返回平铺的消息数组,每条带
session_id、role、text、object
- 展开逻辑:遍历所有会话 → 每个轮次 → 用户消息 + 机器人消息列表
- 为历史消息渲染补充两个转换方法(把领域消息转成接口消息)
关键设计决策
| 决策 | 为什么 |
| session_id 是后端概念,divider 是前端概念 | 分割线("页面打开前/后"的边界)是浏览器里的状态,数据库不知道;所以后端只管返回数据归属,前端负责展示分割 |
| 接口返回平铺数组 | 前端展示需要的是消息流;会话归属通过每条消息的 session_id 保留,方便未来做按会话折叠/分组 |
| 会话归属由外层补 | 消息模型不自己记录 session,展开时由遍历代码补进去 |
验证(里程碑 ⭐ 项目完成)
- 聊天几轮后刷新页面 → 历史完整显示
- 首次进入不显示分割线;发送新消息后,历史与新消息之间出现分割线
- 跨会话(超 60 分钟再聊)能正确分属不同 session
附:开发总流程验收清单
| 阶段 | 里程碑 | 验收一句话 |
| 0 准备 | 环境可用 | 前端打开、LLM/DB/电商接口都通 |
| 1 领域模型 | 数据形状确定 | 消息/上下文/状态模型自测通过 |
| 2 流程定义 | 业务可配置 | YAML 能加载成流程对象,坏配置启动即报错 |
| 3 三层骨架 | 链路通 | POST /api/chat 真实读写数据库 |
| 4 真引擎 | 主干通 | "我要退款"→ LLM 产出 start_flow 指令 |
| 5 防幻觉 | 有安全网 | 编造流程被拦截;点卡片三场景正确 |
| 6 命令执行 | 生命周期完整 | 打断/取消/恢复对话状态正确切换 |
| 7 动作体系 | 能干活 | 查订单/物流接口并写回槽位 |
| 8 流程执行器 | 核心闭环 | 退款/查单流程端到端跑通 |
| 9 知识+闲聊 | 全功能闭环 | 问政策、点卡片问商品、闲聊都能答 |
| 10 历史对话 | 项目完成 | 刷新可见历史,分割线正确 |
附:每阶段最容易踩的坑
| 阶段 | 常见坑 |
| 0 | 异步驱动不匹配;.env 配置遗漏;连接池没在生命周期里初始化 |
| 1 | 联合类型序列化失败(子类标签不匹配);可变默认值共享 |
| 2 | YAML 缩进/冒号后空格;忘了 safe_load;重复槽位静默覆盖 |
| 3 | 用了同步驱动阻塞事件循环;upsert 用了标准 insert 没有 on_duplicate_key_update |
| 4 | prompt 字段漏传导致 LLM 瞎编;历史取太多超 token(用最近 10 轮) |
| 5 | 只校验不回复(职责混淆);对象消息直接改状态绕过统一链路 |
| 6 | start_flow 分支漏掉"同流程重复启动";resume 失败没回退 |
| 7 | Action 直接改 state 而非返回结果;接口异常未降级 |
| 8 | action 步骤没先推进导致死循环;collect 没做自动补槽 |
| 9 | provider 接口不统一;FAQ/RAG 检索失败直接抛异常 |
| 10 | 忘记带 session_id;把分割线逻辑做进后端 |