【电商小二】详细开发总流程

文章目录

    【电商小二】详细开发总流程

    本文按笔记 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:准备

    做什么

    1. 还原两个数据库脚本(客服库 + 电商库)
    2. 启动模拟电商后端(ecommerce-service-backend,提供订单/物流/商品接口)
    3. 启动前端页面(customer-service-frontend,教学可视化控制台)
    4. 创建核心工程 customer-service-backend:目录结构、.env 配置、pyproject.toml、安装依赖
    5. 搭好三个基础设施: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:领域模型 —— 先定"数据的形状"

    做什么(三个文件,由小到大)

    1. 消息模型:用户消息、机器人消息、聚焦对象(订单/商品卡片)、消息类型、引擎返回值
    2. 任务上下文模型:业务任务快照 + 系统过场快照(基类 + 5 个子类 + 反序列化适配器)
    3. 对话状态聚合根:把消息、任务、系统过场、聚焦对象、历史会话全部装进一个 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:流程定义与加载 —— 让业务"可配置化"

    做什么

    1. 学习 YAML 语法(缩进、键值对、列表、嵌套、多行文本、安全加载)
    2. 定义流程数据模型:链接(静态/条件/兜底)、步骤(start/end/action/collect)、容器(流程 + 全局槽位)
    3. 写加载器:把 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:三层骨架 —— 让链路先"通"起来

    做什么

    1. 两套模型分工:接口模型(给前端看的契约)vs 领域模型(内部业务),Web 层负责翻译
    2. ORM 表映射:一张表、两列(用户 ID 主键 + 整份状态 JSON)
    3. Repository 层:读取状态(查不到就返回全新空状态,上层不用区分新老用户)/ 保存状态(upsert,一条语句搞定插入或更新)
    4. 占位引擎:不智能,只回固定话,但方法签名和真引擎完全一致
    5. Service 层:三步编排——加载状态 → 调引擎(引擎就地改状态)→ 保存状态
    6. 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 规划 —— 让"理解"发生

    做什么

    1. 真引擎五步框架:准备会话 → 开启本轮 → 按消息类型分流 → 回复写入并提交 → 组装返回
    2. 会话管理:无会话开新;超过 60 分钟无活动→ 关旧会话、重置运行时状态(清任务/挂起/聚焦对象)、开新会话;未超时则续期
    3. 规划产物数据模型:TurnPlan(三个轨道字段)+ Command(4 种原子指令)
    4. TurnPlanner:把对话状态组装成 7 个 prompt 字段 → 渲染 Jinja2 模板 → LLM 调用链(模板 | 模型 | JSON 解析)→ 解析成 TurnPlan
    5. HistoryBuilder:把历史对话渲染成文本喂给 LLM(对象消息渲染成 [订单对象 id=..., title=...] 描述)

    为什么这样设计

    决策为什么
    引擎只调度、不干活引擎持有一堆"专员"(planner/各类 handler),自己只决定走哪条轨道、派活给谁——职责清晰、易扩展
    LLM 输出结构化 JSON不直接让 LLM 自由发挥,而是让它填 task/knowledge/chitchat 三选一 + 结构化指令,代码才能可靠执行
    4 种 Command回顾用户办业务的所有动作,就四类:发起 / 补信息 / 放弃 / 回头继续——"自然语言动作 → 原子指令"
    7 个 prompt 字段分三组:判断输入(这句话、最近 10 轮历史)、当前状态(活跃任务、挂起任务、聚焦对象)、可选范围(可用流程清单、知识意图清单)。给 LLM 看什么,决定它判断得准不准;可选范围字段就是"给 LLM 划圈"防止瞎编
    流程清单去掉 stepsLLM 只需要"业务说明书"(有哪些流程、干什么的),不需要内部步骤——省 token 又防干扰
    先不做校验这是"先主干、再加固"的刻意取舍:正常路径先通,防护留到下一阶段

    验证(里程碑 ⭐ 主干通)

    • 说"我要退款"→ LLM 返回 {"task": {"commands": [{"command": "start_flow", "flow": "refund_request"}]}} → 进入 task 轨道入口(内部是占位,先回固定话)
    • 说"今天天气不错"→ 观察 LLM 是否走 chitchat(此阶段先不实现,能看到规划结果即可)

    阶段 5:防幻觉 + 对象消息 —— 安全网与"另一只手"

    做什么

    1. 校验器:检查 TurnPlan 能不能执行,不能就给一个"原因码"(9 种)
    2. 对象消息处理:点卡片的三场景处理 + 判断"对象能否填当前槽"
    3. 澄清回复器:原因码 → 基础话术 → LLM 润色 → 追问用户
    4. 知识意图注册表:定义 7 类知识意图(商品/订单/政策/规则…),为知识轨道做铺垫

    为什么做这一步

    • LLM 是概率模型,输出不保证合法:可能三个轨道全 null、可能编一个不存在的流程 ID、可能一次开两个流程——直接拿去执行会崩溃。上一阶段刻意跳过的"防护",现在补上
    • 用户不只打字,还会点卡片(订单/商品)——这是完全不同的输入形态,此前一直没处理

    关键设计决策

    决策为什么
    9 种原因码分三组轨道层(没判断出方向/多个方向)→ task 内部(命令为空/类型不合法/多个流程/流程不存在)→ knowledge/对象(意图为空/缺聚焦对象/只发对象没说要干嘛)
    校验只判断、不回复校验器只输出 valid + reason,话术交给澄清回复器——职责单一,互不污染
    多意图不强选命中多条轨道时不替用户猜,触发澄清让用户决定先办哪个
    最典型的防幻觉检查:流程必须真实存在LLM 可能把 refund_request 编成 refund 或 tuikuan——拿真实流程清单查证,查不到就是幻觉
    对象消息不经过 LLM点一个订单的语义是明确的,不需要"理解",与文本消息本质不同
    对象消息三场景能填当前缺的槽→填槽推进;没流程→澄清意图;在流程但槽不匹配→不打断,让流程继续
    对象归一化为命令点卡片填槽也构造 SetSlotsCommand,与打字填槽走同一条链路,复用流程推进逻辑
    澄清"规则保底 + LLM 润色"先按原因码写死一句兜底话术(保证永远有话说),再让 LLM 润色得更自然(保证说得好听)

    验证

    • 故意让 LLM 输出不存在的流程 → 校验拦截,返回澄清追问
    • 三场景点卡片:流程缺订单号时点订单(填槽推进)/ 无流程时点订单(澄清)/ 流程收退款原因时点订单(不打断)
    • 澄清回复自然贴合上下文(带上了订单号等细节)

    阶段 6:命令执行 —— 任务生命周期

    做什么

    1. TaskHandler:两阶段协调者——先让命令处理器改状态,再让流程执行器推流程(后者下阶段才实现,先占位)
    2. CommandProcessor:把 4 种命令真正应用到状态上

    关键设计决策

    决策为什么
    命令逐条按序应用一轮可能多条命令("我要退款,订单号 A001"→ start_flow + set_slots),顺序有意义:必须先建任务,槽位才有地方填
    start_flow 五种处境有没有活跃任务 × 目标在不在挂起栈,决定:新建 / 从栈恢复 / 打断当前再新建 / 打断后恢复目标 / 重复启动直接忽略
    改状态 + 激活过场成对出现每次改任务状态都顺手激活一个系统过场(开始/打断/取消/恢复),让系统说出贴合处境的话——过场文案在 system_flows.yml 里
    resume 支持指名与不指名指名按 flow_id 精确恢复;不指名("继续刚才的")恢复栈顶——LIFO 贴合"接着最近那件事"的对话直觉
    resume 失败回退先打断当前任务、再尝试恢复目标,若目标不在栈中则把刚打断的任务恢复回来——"要么全做、要么撤销"
    取消 vs 打断打断→进挂起栈可恢复;取消→直接丢弃。这是任务生命周期最需要分清的一对
    空命令列表照常推进对象消息"槽不匹配不打断"场景传空命令——命令处理器什么都不做,直接进入流程推进

    验证(核心:连续对话推演)

    跑"我要查订单状态 → 先帮我查物流 → A001 → 算了还是看订单状态吧",逐步观察 active_task 与 paused_tasks 像栈一样此起彼伏,系统过场话贴合每步切换。


    阶段 7:动作体系 —— 让系统"干实事"

    做什么

    1. 框架四件套:动作基类(名字 + 异步 run 方法)、动作结果(回复消息 / 槽位更新 二选一)、动作注册表(名字→实例)、动作执行器(查表→调用→返回)
    2. 内置动作:action_response(三种回复模式)+ action_listen(哨兵,等用户输入)
    3. 自定义动作:查订单状态、查物流、推荐相似商品(占位)+ 调电商接口的共享工具
    4. 自动注册:内置手动注册;自定义目录启动时自动扫描注册

    为什么做这一步

    • 前面所有阶段都在改"状态",但没有任何代码真正干过活:没查过订单、没调过接口、没生成过回复文本。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 轨道闭环

    做什么

    把之前所有"零件"(流程定义、状态、命令、动作)串起来跑:

    1. 两层循环:外层管动作执行,内层管步骤推进
    2. 四种 step 的处理:start(推进)/ end(清任务)/ collect(补槽判断)/ action(推进 + 构造调用请求)
    3. 条件跳转:表达式求值(受限 eval)+ 三种链接的选择
    4. 自动补槽:collect 步骤先检查聚焦对象能否直接提供槽位

    关键设计决策

    决策为什么
    两层循环 + action_listen 哨兵内层沿 YAML 图推进到"下一个要执行的动作",外层执行动作、把槽位更新写回状态、收集回复;遇到哨兵就退出等用户——推进与执行是两种粒度,拆开避免每步都做类型判断
    系统任务优先当前任务返回时系统过场优先(一行 or 短路)——过场/补槽追问"覆盖"在业务流之上执行,不需要复杂的挂起恢复框架
    action 步骤先推进再执行先改 step_id 再返回动作调用,否则执行完会停在同一个 action 上死循环
    collect 复用系统过场激活补槽过场时把问句作为参数传入;过场内部用 context.response 间接引用取问句——一个系统流服务所有业务 collect 步骤
    自动补槽解决"用户先点卡片、后说意图"的跨轮场景:聚焦对象跨轮持久化,进 collect 步骤主动检查、直接填入,省掉一次追问
    受限 eval条件表达式用 eval 执行,但屏蔽内置函数、只暴露 slots 和 context 两个变量——安全

    验证(里程碑 ⭐ 核心闭环!)

    • 完整跑通退款流程:"我要退款" → "好的,我们先处理退款申请" → "请告诉我你的订单号" → 用户给号 → "请简单说一下退款原因" → 用户给原因 → "退款申请已提交,原因是……"
    • 查订单流程:先点订单卡片再查(验证自动补槽)
    • 条件分支:推荐商品流程无商品时走引导分支
    • 整个客服系统的核心逻辑——业务任务处理——至此实现完毕

    阶段 9:补全知识 + 闲聊轨道 —— 三轨道完整

    做什么

    1. 闲聊轨道(轻):取"用户消息 + 最近历史" → LLM 自由回复。没有流程、没有检索、没有命令
    2. 知识轨道(重):意图 → 知识来源 → 检索 → 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:历史对话 —— 刷新可见历史

    做什么

    1. 历史接口:返回平铺的消息数组,每条带 session_id、role、text、object
    2. 展开逻辑:遍历所有会话 → 每个轮次 → 用户消息 + 机器人消息列表
    3. 为历史消息渲染补充两个转换方法(把领域消息转成接口消息)

    关键设计决策

    决策为什么
    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联合类型序列化失败(子类标签不匹配);可变默认值共享
    2YAML 缩进/冒号后空格;忘了 safe_load;重复槽位静默覆盖
    3用了同步驱动阻塞事件循环;upsert 用了标准 insert 没有 on_duplicate_key_update
    4prompt 字段漏传导致 LLM 瞎编;历史取太多超 token(用最近 10 轮)
    5只校验不回复(职责混淆);对象消息直接改状态绕过统一链路
    6start_flow 分支漏掉"同流程重复启动";resume 失败没回退
    7Action 直接改 state 而非返回结果;接口异常未降级
    8action 步骤没先推进导致死循环;collect 没做自动补槽
    9provider 接口不统一;FAQ/RAG 检索失败直接抛异常
    10忘记带 session_id;把分割线逻辑做进后端
    end