中文 · English
打开在线演示 ↗(不用安装,选一个角色进入:顾客、坐席或管理员;回放真实模型或离线模型的录制,实际来源以页面标注为准)· 代码 · 5 分钟用真实模型跑起来 · 案例:一位顾客的退款,从提问到钱退回去 · 架构 · 评测 · 语音通道
每一句回答都有出处,每一个高风险动作都要人复核,每一次人工纠错都会变成回归测试。
关键词 / Keywords:企业客服 Agent · RAG 知识库问答 · 工具调用(订单 / 物流 / 退款 / 工单)· 人工审批 human-in-the-loop · 逐句证据校验 · 数据飞轮 · 回归评测门禁 · 语音通道 Voice Agent · MCP · FastAPI + React
LoopDesk 是一个面向企业客服场景的 Agent 平台:知识库问答(RAG)、订单 / 物流 / 退款 / 工单工具调用、人工审批、逐句证据校验、数据飞轮和回归评测,都放在同一个可观测的系统里。
演示场景:主场景是虚构的音乐流媒体平台 Echo音乐,参照同类音乐 App 的会员客服设计:会员连续包月与自动续费退款、取消续费(含 App Store 渠道)、数字专辑、Echo币与未成年人误充值、歌曲变灰与版权、无损音质与离线下载。另外两个租户说明同一套平台换行业也能用:电商 Leon优选(实物订单、物流、七天无理由退货)和 SaaS Iris云文档(帮助中心与 SLA)。
作者:Leon Wang · 个人项目 · 仓库中的企业、订单与客户数据全部为虚构(SYNTHETIC),与任何真实企业无关。
上图(已保存的真实模型录制,Echo音乐 的管理员):问“怎么取消自动续费?”,
deepseek-v4-1-flash先调用检索工具,再按知识库回答,每句带出处编号,证据校验逐句核对后判为“证据充分 · 100%”;右侧是这次运行:快模型、7.0 秒、3,067 token。页面顶部写明回答来自哪个模型、哪天、从哪个提交录制。
企业愿意把客服交给大模型,但上线后通常会卡在三件事上:
| 痛点 | 后果 | LoopDesk 的做法 |
|---|---|---|
| 幻觉:退款时效、费用、额度答错 | 直接引发客诉 | 证据绑定:回答按句拆开,引用必须存在,数字必须与证据一致;严格模式下不达标的回答不会发出 |
| 越权与资损:Agent 自己把钱退出去 | 资金风险,无法追责 | 可复核动作:工具按风险分级,超出租户额度的退款进入审批队列,并附上 Agent 的证据包;审批人不能是申请人本人,大额退款只有管理员能通过,驳回必须写明原因 |
| 修了又犯:bad case 靠人记 | 质量不可控,改 Prompt 怕出问题 | 数据飞轮 + 回归门禁:一次纠错同时生成知识补丁和评测用例;评测集进 CI,通过率低于阈值就阻止合并 |
最近的迭代见 CHANGELOG.md:四轮“找茬—修复—复核”(37 个问题,已修复 28 个)和两次独立代码审查、模型故障兜底(规则直达人工、引用原文的降级回答、熔断)、审批分级与员工操作审计、工单工作台、多进程部署下的正确性(共享登录限流、飞书事件去重与收件箱)、知识冲突未核对时转人工、检索随知识库规模扩展(2 万片段 391ms → 10ms)、管理操作审计与防篡改、OpenTelemetry 导出。
(截图由新版控制台回放仓库保存的 Echo音乐 真实模型 deepseek-v4-1-flash 录制生成,脚本为 frontend/scripts/screenshots.mjs。线上站点按兼容性检查选择真实模型或离线模型录制,实际来源以页面标注为准;检索调试器和下面的证据校验演示取自电商租户 Leon优选 的本地运行。)
Agent 与 RAG
- 自研 Agent 循环(不依赖框架):流式输出、多步工具调用、最大步数保护、模型降级重试。
- 模型故障兜底:“转人工”“我要投诉”这类明确要求由规则识别,不经过模型直接转人工;两档模型都失败或熔断打开时,知识问题直接引用政策原文并注明出处,涉及订单或无可引用内容的转人工;每个模型一个熔断器,失败的模型暂停调用,顾客不再挨个等超时。
- 运行不会悬空:流式运行与连接解耦,顾客关掉页面也会跑完;运行时自身出错时把运行记为失败;进程被杀掉(kill -9、OOM、容器回收)留下的“运行中”记录,由
python -m app.ops.stale_runs(每分钟一次,实例启动时也执行一次)在超过时限(运行时限加上它管不到的追问改写和 LLM 校验,再留 60 秒,默认 290 秒)后记为失败并导出链路,顾客还在等这次回答时在对话里留一条系统提示;审批恢复途中被杀掉的,退款没执行就把审批单退回待审批(记审计,再次通过只付一次),执行了就把退款结果告诉顾客,驳回则保留并告知审核意见。按时间判断,多实例部署时不会误伤别的实例正在执行的运行。 - 混合检索:jieba 分词 + BM25 与向量检索,用 RRF 融合;IDF 加权的“词覆盖率”作为绝对相关度,最关键的词缺失时 Agent 会拒答,而不是答非所问。
- 查询归一化:先把异体字和常见错别字统一(“到帐”改为“到账”),再用小型领域同义词表把口语短语扩展成知识库里的标准说法(“专票”扩展为“增值税专用发票”,“多掏钱”扩展为“额外支付 运费”),只追加、不替换。新增的 5 条口语评测用例在修复前全部检索失败,修复后全部通过。相关度门槛(词覆盖率)则改用替换后的标准说法计算:“能寄到国外吗”里的“寄到”“国外”不在知识库词表里,IDF 最高,留在分母里只会把分数拖到阈值以下,所以门槛按“配送 海外”打分;召回仍用追加后的完整查询。另一批 5 条口语用例(“能开发票吗”“退货运费谁出”等)由此从拒答或答错变为正确。
- 按标题切分文档并保留章节路径;入库时扫描提示注入和诈骗话术(先做全角、空格、零宽字符归一化,繁体折叠成简体;索要验证码或银行卡号、私人微信、转账押金),命中的片段不进入检索,带原因等管理员复核放行。
- 输出检查:回答发给顾客之前再查一次是否索要验证码、银行卡号、密码,或要求先付钱才能退款;回答里出现租户官方渠道(域名、客服账号)以外的链接或账号一律不发。流式输出按句放行;命中就不发、转人工,看板列出知识库里的来源文档。盲测集(看不到规则的模型写的 69 条攻击、82 条正常文本)上入库扫描拦住 45/69、误扣 7/82,要钱类只有 2/12,见 EVALUATION。
- 知识冲突检测:文档入库或补丁发布时,比对其他文档中同一主题、带单位的数字(天、元、积分……),发现“1-2 个工作日”和“1-3 个工作日”这类矛盾就记录下来并提醒管理员,而不是等顾客拿到错误答案;核对之前,同时用到两种说法的顾客问题转人工,不让 Agent 把两个版本都说出来。管理员可以在线修改文档,保存时用版本号做乐观锁,两人同时编辑不会互相覆盖。
- 多轮追问改写:识别“那运费谁出?”这类追问,结合上一问改写成完整问题再交给 Agent(真实模型下用快模型改写,离线时用规则改写)。对话中保存用户的原话,链路中记录改写结果。对应的多轮评测用例在修复前失败,修复后通过。连续追问时,参照的是上一轮改写后的问题,而不是原话;离线规则把对话拆成“话题”和“问题”两部分记下来:“那运费谁出?”保留话题、换问题,“那质量问题呢?”换话题、保留问题,“还有能开发票吗”是新问题,只去掉开头的“还有”。
- 权限感知检索:内部文档只对员工可见。
- 知识版本感知的答案缓存:重复的 FAQ 直接复用已经通过证据校验的回答,命中时消耗 0 token。缓存键包含知识库版本签名、提示词和模型配置指纹以及可见范围,所以知识一更新缓存就自动失效,内部答案也不会经缓存泄漏给顾客。缓存键不能替路由做决定:键只折叠全半角、大小写和连续空白,句中的空格和标点保留,拒答不缓存,所以一个顾客的“我 要 退 款”不会让其他人的“我要退款”拿到缓存里的拒答(N-116)。顾客对缓存回答(或被存下的那条回答)点“没帮助”或坐席纠错,这个问题的缓存立即作废,知识库或配置变更之前不再缓存;差评按产生原答案的那次运行归因;管理员可以用
GET / DELETE /api/answer-cache查看命中最多的条目并清除(N-097)。
人机协同
- 转人工是一个状态,而不是一句话术:同一会话只开一张转人工工单(重复要求不会重复排队),排队位次只计算排在前面、尚无人接待的顾客。会话转人工后 Agent 不再回答,顾客继续发的消息原样留给人工,并告知当前位次或接待人,既不调用模型,也不虚增转人工率。
- 工单工作台:Agent 建的工单按对顾客承诺的时限排序,显示剩余时间或已超时,坐席接手、写明处理结果后关闭,结果自动发回顾客的对话;承诺时限在建单时记下,改租户 SLA 不会改写已经告诉顾客的时间;看板提示超时工单。
- 人工接待页:坐席按等待时间先后接待,第一个回复的人负责跟进,可以随时结束人工服务,把对话交还给智能客服;交还后人工的回复会作为上下文带给 Agent。飞书渠道的人工回复会直接发回飞书会话。
语音通道(Voice Agent)(设计 · 评测 · 难点)
- 级联:VAD 判断说完(按音频时长计时,按住说话松开即结束)→ 流式 ASR(Paraformer 流式出中间结果,一句结束后 SeACo-Paraformer 带租户热词整句重解码)→ ITN(“S O 二零二六……”→
SO2026…,“一百九十八块”→ 198)和热词拼音纠错 → PII 脱敏 → 同一个 Agent 运行时(检索、工具、审批、证据校验、审计都不变)→ 输出检查放行一句就送去流式 TTS,检查不占首包时间。 - 资金动作口头确认:语音通道给运行时换一份工具注册表,退款这类
risk=high的工具先把规范化后的订单号和金额念给顾客,金额那句播完、顾客说“确认”才执行,一次确认只放行这一组参数一次,之后照常走审批;确认记进审计。订单号或金额没把握(两遍识别不一致、格式不对)先复述再办。 - 顾客听到什么,对话就记什么:打断时立即停播,助手消息只保留已经播出的部分,模型下一轮不会以为没播的话已经说过;断线算“不确定是否听到”,不算打断。手机号、卡号、身份证、验证码不念出来。
- 语音模型是可选依赖,测试和 CI 不装;没有引擎时对话页不显示语音按钮。评测在 GitHub Actions 上跑:107 条门禁用例 × 8 种合成音色和语速 + babble 噪声,Paraformer / SenseVoice / Whisper 对比,热词和 ITN 前后对比,语音与文字的任务通过率和失败归因,端点检测和 TTS 时延(见下)。
安全与治理
- 多租户隔离:跨租户访问一律返回 404,避免通过 ID 探测数据。
- RBAC:管理员 / 客服坐席 / 顾客三种角色。权限矩阵测试会遍历 OpenAPI 中的每个接口:除了明确列出的公开接口(健康检查、登录、带校验 token 的飞书回调等),都必须要求登录;顾客只能调用列出的对话类接口。新增接口忘了加权限,测试会直接失败。
- 工具层鉴权:身份取自登录态,而不是模型生成的参数,所以 Prompt 注入也查不到别人的订单。
- 审批通过后,执行前再校验一次订单状态(防 TOCTOU);同一订单不能重复提交待审批的退款申请。
- 售后时效由工具层判断,不靠模型自觉:签收 7 天内按规则自动处理;7 到 15 天只有质量问题可以退,工具看不到顾客上传的照片,所以一律转人工核实;超过 15 天直接拒绝,不进审批。期限按顾客提出申请的时间计算,审批排队不会让顾客因此超期。两个期限是租户配置(
PATCH /api/tenant)。 - 累计退款风控(防“薅羊毛”):同一顾客 30 天内已退款 3 笔或累计超过 200 元时,即使单笔低于自动审批额度也要人工审核。统计按顾客而不是按操作人,坐席代为申请也绕不过去;几笔同时提交也绕不过去(执行时按顾客加锁,在锁内重新统计)。
- 手机号、身份证、银行卡、邮箱在进入模型、写入数据库和链路之前脱敏,覆盖人们实际的输入习惯(
138-1234-5678、+86 138 1234 5678、每 4 位一组的卡号);不只是聊天消息,反馈意见、会话标题、审批意见、工单处理结果、人工回复这些自由文本入口也一样(有一条测试读遍所有表来核对);接口带限流。 - 控制台设计:产品介绍页 + 深色工作区(分区标签各有强调色,手机端底部标签栏),版式参照电商后台和客服工作台,自建设计系统(沿用 Semi Design 的令牌体系,没有组件库依赖);文字和填充色两种主题下都过 WCAG AA 检查,对话框有焦点陷阱,见 docs/DESIGN.md。
- 浏览器加固:所有响应都带
nosniff、X-Frame-Options: DENY、Referrer-Policy,以及只允许同源资源的 CSP(打包后的控制台已在该 CSP 下逐页验证,没有报错)。 - 时区:时间一律按 UTC 存储和比较;给人看的日期(看板的每一天、告诉顾客的付款和签收日期)按租户时区(
PATCH /api/tenant的timezone,IANA 名称,默认 Asia/Shanghai)。早上 6 点半签收的订单,不会被说成前一天签收。 - 数据保留:
python -m app.ops.retention --days 90删除超过保留期的链路明细,并抹除对话正文、运行记录和已关闭工单中的文本;指标、审批和退款等审计记录保留;还在等待审批的运行、还没处理完的工单不会被处理。 - 操作审计:管理员改风控额度、售后期限、知识文档、补丁、冲突、评测用例、执行删除权,以及员工通过或驳回审批、导出数据、人工接待回复和结束服务、接手和处理工单,都会记下操作人、时间、请求 ID 和做了什么(配置记改前改后,文档记修改的行,审批记金额和意见,导出记天数和行数),在「操作审计」页面按类别查看;删除权的记录只保留账号 ID,人工回复只记消息编号,知识补丁只记长度和 SHA-256(内容留在对话里,受保留期约束)。审批的记录和审批决定在同一个事务里提交,执行失败退回待审批时再记一条。每条记录带租户内连续编号和 HMAC 签名(密钥不在数据库里),页面上显示完整性校验结果,直接改库改写或删除记录会被发现。
- 登出与强制下线:退出登录会调用
POST /api/auth/logout,服务端让该用户已签发的凭证全部失效(凭证带版本号,退出时加一),从浏览器、代理日志里拿到的旧凭证不能再用;管理员可以用POST /api/admin/users/{id}/sign-out让某个用户所有设备下线(丢了电脑、凭证泄露),记入操作审计。代价是退出时所有设备一起退出。 - 员工离职:管理员用
POST /api/admin/users/{id}/disable停用员工账号,原密码不能再登录、已签发的凭证立即失效,其名下未关闭的工单和人工接待退回队列(等别人接手),姓名保留,审批、工单、导出和审计里仍然显示是谁处理的;POST .../enable恢复,GET /api/admin/users列出员工和停用状态。员工账号不能执行删除权(返回 400):抹除会把账号改名,历史上所有经手记录都会变成“已注销用户”。 - 删除权:管理员可以调用
DELETE /api/admin/users/{id}/personal-data抹除某个用户的个人数据,包括对话、运行记录、工单里的文本(标题、描述是顾客原话,处理结果是写给顾客的)和针对其消息的反馈,并将账号匿名化、使其无法登录,已签发的登录凭证也立即失效(凭证带版本号,抹除时加一);订单、退款、审批单作为财务和审计记录保留,只留账务字段:审批证据和除订单号、金额以外的全部工具参数(模型常把顾客原话抄进退款理由,姓名、住址不在脱敏范围内)、退款理由、由其反馈生成的评测用例(归档)和没采纳的知识补丁都会清除,相关的答案缓存删除。两处例外写在返回值里:还可能重发给支付系统的退款(claiming、pending)保留理由,因为重试和对账必须发送同一个请求,有了结果后再执行一次即可清除(refunds_unsettled);已采纳进知识文档的补丁只计数(kb_patches_applied),由管理员修改文档。被抹除的内容统一显示“[应用户要求已删除]”,和保留期的“已过保留期”区分开,之后的保留期清理也不会改写它。有测试对全库每张表搜索顾客说过的姓名和住址,抹除后一条都不能剩。抹除前先收尾:该顾客的待审批单自动驳回(不动钱,记在执行抹除的管理员名下),排队中的转人工和未关闭的工单随之关闭。
质量闭环
- 数据飞轮:纠错 → 失败归因(知识缺失 / 检索未命中 / 无依据 / 工具错误)→ 候选评测用例 + 知识补丁草稿 → 管理员发布 → 重建索引并激活用例。
- 回归评测在隔离沙箱库中运行:知识库和租户配置取自当前环境,订单数据取自固定夹具,所以演示中的真实退款不会把用例变红。
- Prompt 版本化:提示词按版本登记,评测 CLI 支持
--compare 1.00,2.00做离线 A/B(任何用例回归即失败);线上按会话哈希稳定分流做灰度,看板按版本对比转人工率、证据覆盖率和 token。离线 A/B 中精简版 v2 的 token 减少 31%,并且没有回归;它对回答质量的影响需要用真实模型验证。
集成
- 模型:豆包(火山方舟)、混元、DeepSeek、通义、OpenAI 统一走 OpenAI 兼容协议;Claude 使用官方 SDK;没有 Key 时使用离线确定性模型。
- 渠道:飞书机器人事件回调(challenge 校验、事件去重(记在数据库里,多 worker 和重启后都有效)、先落库再在 3 秒内回 200、后台处理失败时记录日志并回复兜底话术;处理中的进程崩溃时,补偿任务会重新认领并回答,最多重试 3 次,之后回复兜底话术)、企业微信群机器人审批提醒。
- 订单与支付系统:查订单、查物流、退款通过
OrderGateway/RefundGateway访问外部系统,默认用演示表;按租户配置LOOPDESK_COMMERCE_APIS后接商家自己的 HTTP 接口(Bearer 令牌、可选 HMAC 签名、只重试幂等请求、回复不合约定按故障处理,每次调用记进链路)。退款先写记录再请求支付系统,随机幂等键在第一次请求前写入;支付系统没有回音时不算失败,订单继续占着,顾客被告知“正在核实”,由python -m app.ops.refund_reconcile用同一个键重发,只会退一次;确认不了的变成工单,管理员核实后记下结果(记审计)。接口约定见 DEPLOYMENT。 - MCP:只读工具通过 MCP 暴露,Claude Desktop、Cursor 等客户端可以直接接入。
- 租户自己的工具:管理员用接口声明 HTTP 工具或接入 MCP Server(风险等级、审批策略、出站白名单、密钥引用),在控制台「工具集成」页看调用次数、最近错误、待审批单并修改策略;资金类工具不能比租户的退款分级更松。演示租户 Leon优选 预置“积分查询”和“补偿券”(20 元以内自动发放,超过走审批),对接虚构的
api.leon.example后台。 - 运维:Prometheus
/metrics、Docker、一条命令起全栈的 docker-compose(PostgreSQL)、Kubernetes 清单(deploy/k8s:探针、HPA、PDB、迁移 Job、定时任务,CI 用 kubeconform 校验,并在 kind 集群上实际部署、经 Service 问一个问题)、GitHub Actions(SQLite + PostgreSQL 双库测试、评测门禁、Playwright 端到端走查、docker compose up冒烟、手动触发的真实模型评测和演示录制)。 - 数据库迁移:Alembic 管理表结构版本,启动时自动升级;对旧版本用
create_all建的库会补齐缺失的表并标记为最新版本。CI 在 SQLite 和 PostgreSQL 上都会校验“模型定义与迁移脚本完全一致”。 - OpenTelemetry:配置
LOOPDESK_OTLP_ENDPOINT后,每次运行导出为一条 OTel trace(invoke_agent→chat <模型>/execute_tool <工具>,GenAI 语义约定,含 token 用量),可直接接入 Jaeger、Grafana Tempo、Langfuse;只导出元数据,不导出对话内容。 - 可排障:每个请求都带
X-Request-ID(可以沿用网关传入的 ID),它会写进 JSON 结构化日志和 Agent 运行记录;发生 500 错误时只返回 request_id,不暴露内部信息;/api/ready检查数据库和迁移版本,供 Kubernetes 和 Docker 的健康检查使用,/api/health用作存活检查。 - 数据导出:对话记录、反馈、审批可按天数导出为 CSV(带 UTF-8 BOM,Excel 打开不乱码;对以
=+-@开头的单元格做转义,防止 CSV 公式注入;按租户隔离,导出的文本是已脱敏的版本)。
flowchart LR
subgraph Channels
W[Web 控制台<br/>React + TS]
F[飞书机器人]
M[MCP 客户端]
end
W -- SSE --> API
F -- 事件回调 --> API
M -- stdio --> MCP[MCP Server]
API[FastAPI<br/>鉴权 / 多租户 / 限流] --> RT
subgraph RT[Agent Runtime]
G[输入护栏<br/>PII 脱敏 / 注入检测] --> R[模型路由<br/>快 / 强 / 降级]
R --> L{LLM}
L -- tool call --> P[策略检查<br/>预校验 / 审批阈值]
P -- 需要人工 --> AQ[(审批队列)]
P -- 放行 --> T[业务工具]
T --> L
L -- 最终回答 --> V[证据校验器]
end
T --> KB[(混合检索<br/>BM25 + 向量 + RRF)]
T --> BIZ[(工单 / 退款记录)]
T --> GW[订单与支付网关] --> OMS[(演示表 / 商家自己的<br/>订单和支付系统)]
AQ -- 人工决定 --> RT
T -- 转人工 --> HQ[(人工接待队列)]
API -. 人工处理中的会话直达坐席 .-> HQ
HQ -- 结束服务,交还 Agent --> RT
RT --> TR[(Trace / Span)]
V --> FB[反馈与纠错] --> FW[数据飞轮]
FW --> KB
FW --> EV[(评测用例)] --> CI[回归评测 / CI 门禁]
设计细节见 docs/ARCHITECTURE.md。落地时会遇到的问题(以 AI Agent 特有的问题为主:幻觉、工具调用、提示注入、评测可信度、非确定性、延迟成本、模型故障、长时间运行的状态)见 docs/CHALLENGES.md。
只需要 Docker 和一个 OpenAI 兼容接口的模型 Key(DeepSeek、豆包、通义、混元、OpenAI,或者经网关接入的任何模型):
git clone https://github.com/leon1111-wgl/loopdesk.git && cd loopdesk
cat > .env <<'EOF'
LOOPDESK_LLM_BASE_URL=https://api.deepseek.com/v1
LOOPDESK_LLM_API_KEY=<你的 Key>
LOOPDESK_LLM_MODEL=<控制台里的模型 ID>
EOF
docker compose up -d --build --wait # PostgreSQL + 应用,就绪后返回
docker compose exec app python -m app.ops.llm_check # 两档模型各发一个最小请求
python3 backend/app/ops/smoke.py http://localhost:8000 --provider openai_compat # 登录、提问,打印回答、模型和 token然后打开 http://localhost:8000,在登录页点一个演示账号(Echo音乐 的顾客、坐席或管理员)。不写 .env 也能跑,那样由离线模型回答(--provider mock)。停掉:docker compose down(加 -v 连数据库一起删)。
这几条命令是验证过的:CI 每次推送都用离线模型执行 docker compose up 和 smoke.py;手动触发的 Real-model eval 工作流用 GitHub Secrets 里的真实模型把上面的命令再执行一遍。2026-10-22 那次:GitHub Actions runner 上从零构建镜像到就绪 49 秒,deepseek-v4-1-flash 回答“怎么取消自动续费?”用了 7.7 秒(2,897 + 167 token,检索后回答,证据校验通过)。生产部署(LOOPDESK_SEED_DEMO=0、自己的密钥、多副本和迁移任务、Kubernetes 清单)见 DEPLOYMENT。
环境要求:Python 3.11+、Node 20+(推荐 pnpm)。不需要任何 API Key。
# 后端
cd backend
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000# 前端(另开一个终端)
cd frontend
pnpm install
pnpm dev # http://localhost:5173 ,/api 会代理到 8000首次启动会自动创建三个演示租户(Echo音乐、Leon优选、Iris云文档)、知识库、订单和几段演示对话。登录页列出了所有演示账号,点击即可进入。
Docker(PostgreSQL + 单镜像):docker compose up -d --build --wait,见上一节。
测试与评测:
cd backend
pytest -q # 全部测试
python -m app.evals.cli --fresh --gate 0.95 # 回归评测,低于 95% 退出码为 1端到端走查(Playwright,真实 API + 构建后的前端 + 全新的演示库):
cd frontend
pnpm build && pnpm exec playwright install chromium
E2E_PYTHON=../backend/.venv/bin/python pnpm e2e四条路径各跨两种角色:顾客提问拿到带证据校验的回答、管理员在链路页找到这次运行;顾客申请超额退款进入审批、管理员通过后顾客页面无需刷新就收到结果;顾客转人工、坐席在人工接待页回复、顾客页面收到回复;顾客报修、Agent 建工单、坐席在工单页接手并写明处理结果、顾客在对话里看到结果。任何页面出现控制台错误、未捕获异常或 5xx 都算失败。CI 中单独一个 job 运行。
在线演示是同一套 React 控制台的静态构建,部署在 GitHub Pages 上,没有后端。录制脚本(python -m app.ops.record_demo)在真实后端上按剧本让顾客提问、坐席审批和回复、管理员改额度、跑一遍回归评测,把控制台会请求的每个接口的响应和示例问题的事件流录下来;浏览器里的 demo/replay.ts 用这些录制回答请求:可以切换角色浏览每个页面,回放对话页的示例问题;提交类操作不会生效。
- 回答来自真实模型:手动触发的
demo-record.yml在 GitHub Actions 上用 Secrets 里的模型录制(--model configured),提交到demo/real-model/fixtures.json并重新部署。真实模型不一定给剧本里的员工步骤留出机会(比如它没有申请审批,坐席就没有审批可做),这样的步骤跳过并记在录制里,不编造。页面顶部和登录页写明是哪个模型、哪天、从哪个提交录的。 - 不会因为代码更新而坏掉:每次推送到 main,
pages.yml仍然用离线模型重新录一份,再比较两份录制:当前控制台要请求的账号、页面、示例问题和响应字段,真实模型的录制都有,才回放真实模型的;缺了就回放离线模型的(横幅会写“离线模型”),并在任务摘要里列出缺了什么。没有配置模型 Key 的 fork 也是离线模型。 - 不泄露凭证:录制和发布前都会扫描模型 Key、网关地址和登录凭证(
app.ops.leak_check),有就拒绝写入;测试还会检查对话页的每个示例问题都有录制。数据全部虚构。
最少三个变量:LOOPDESK_LLM_BASE_URL、LOOPDESK_LLM_API_KEY、LOOPDESK_LLM_MODEL(见上面“5 分钟用真实模型跑起来”)。有地址和 Key 就使用 OpenAI 兼容适配器,快、强两档都用这个模型。需要分档或用预设地址时(完整说明见 docs/DEPLOYMENT.md):
LOOPDESK_LLM_PRESET=doubao # doubao | hunyuan | deepseek | qwen | openai,代替 BASE_URL
LOOPDESK_LLM_API_KEY=...
LOOPDESK_LLM_MODEL_STRONG=<控制台里的模型或接入点 ID>
LOOPDESK_LLM_MODEL_FAST=<更便宜的模型>
# LOOPDESK_LLM_PROVIDER=anthropic # Claude 走官方 SDK,需要显式指定
# LOOPDESK_VERIFIER=llm # 用模型判定措辞是否有依据(数字校验始终开启);带思考过程的模型会明显变慢,见 EVALUATION上线前用 python -m app.ops.llm_check 自检:两档模型各发一个最小请求,失败时说明是密钥、模型名、参数还是网络的问题。接真实模型时演示用的预置对话默认关闭(它会调用十几次模型)。
| 评测集 | 用例数 | 通过率 | 证据覆盖率 | p50 / p95 | 说明 |
|---|---|---|---|---|---|
| Leon优选 default | 67 | 100% | 100% | 3ms / 11ms | 覆盖 RAG、口语与错别字、多轮追问、一句多问、拒答、RBAC、订单工具(含不报订单号时先列出顾客自己的订单)、“先别退款 / 只是问问”不提交退款、撤回待审批的退款申请、审批、售后时效、累计风控、注入攻击,以及 6 条红队用例(冒充审批、伪造系统通知、越权退款、复述卡号、套取提示词、冒充管理员) |
| Iris云 default | 10 | 100% | 100% | — | SaaS 帮助中心:口语问答、拒答、跨租户隔离(知识和订单) |
| Echo音乐 default | 43 | 100% | 100% | 3ms / 8ms | 音乐流媒体:会员价格与自动续费、取消路径(含 App Store)、退款窗口(扣费 3 天内自动、7 天内人工核实、超期拒绝)、假设和英文否定的退款咨询不提交、不能在线退款的商品(Echo币、数字专辑、App Store 扣费)、虚拟商品没有物流、未成年人误充值、歌曲变灰、口语、拒答、跨顾客与跨租户隔离、注入、转人工、工单、员工专用文档 |
真实模型,最新一次(deepseek-v4-1-flash,2026-10-22,提交 0510ecb,手动触发的 Real-model eval 工作流在 GitHub Actions 上跑,每个评测集 3 次;运行记录,详见 docs/EVALUATION.md):
| 评测集 | 平均通过(范围) | pass^3(三次都过) | 每次 token / 估算成本 |
|---|---|---|---|
| Leon优选 default(67 条) | 41.7(40–43) | 30/67 | 26 万 / ¥0.65 |
| Echo音乐 default(43 条) | 30.7(28–33) | 25/43 | 17 万 / ¥0.42 |
| Iris云 default(10 条) | 7.7(7–8) | 7/10 | 3.8 万 / ¥0.09 |
| 困难集(9 条) | 6.7(5–8) | 4/9 | 4.1 万 / ¥0.10 |
整次运行 155 万 token,按公开价估算约 ¥3.7(上限,没扣缓存命中)。三次都失败的 21 条里:10 条是证据校验把正确的句子判成没有依据,4 条是用例要求离线模型的轨迹(例如退款前必须先检索;模型直接退、结果正确),7 条是字面说法对不上;没有退错钱的。所以下一步是评测改为按最终数据状态和语义判分(W1),而不是为这个模型调规则。
真实模型,第一次(DeepSeek V4.1 Flash,2026-10-20,测的是电商和 SaaS 两个租户;每种配置跑 3 次,报平均值和范围):
| 评测集 | 第一次(未改代码) | 修复评分、证据校验和提示词后(默认 1.02) |
|---|---|---|
| Leon优选 default(测量时 52 条) | 26/52 | 37.3(36–39) |
| Iris云 default(10 条) | 6/10 | 7.3(7–8) |
| 困难集(9 条) | 4/9 | 7.3(7–8) |
| 盲测攻击被回答检查拦下(69 条) | 规则 42/69 | 规则 + 模型判定 59/69(“先付钱才退款” 2/12 → 9/12),正常文本误拦不增加 |
第一次的失败大多是评测按离线模型写死了(“7 天”和“7天”算不同;“我帮您查一下”被当成要出处的事实),也查出一个真问题:模型自己判断退款能不能退,不交给工具。修复都来自这套门禁的失败,所以修复后的数字偏乐观;每轮约 16 万 token,p50 约 5.5 秒、p95 约 13 秒。
上图(测试引擎,固定转写 + 提示音):顾客说“S O 二零二六一零一五一零二 年卡我要退款”,转写经 ITN 变成订单号;退款工具前的闸门先把订单号和金额念给顾客,顾客说“确认”后才提交,¥198 超过自动额度进入人工审批。
合成语音(Kokoro 4 个音色、3 档语速,macOS 两个音色),GitHub 4 vCPU 上跑,离线 Agent 固定不变(所以下降全来自转写),详见 docs/EVALUATION.md:
| 识别(线上默认:Paraformer 两遍 + 热词) | 安静 | SNR 20 dB | SNR 10 dB | SNR 5 dB |
|---|---|---|---|---|
| CER | 1.79% | 6.27% | 10.39% | 15.99% |
| 订单号准确率(ITN 后) | 98.2% | 100% | 91.4% | 80.0% |
| 任务通过率:语音 / 文字 | 96.4% / 100% | 93.3% / 100% | 87.6% / 100% | 83.8% / 100% |
- 没有 ITN 时订单号和金额 0% 对得上;第一版 ITN 48.9%(“S O”按汉字念出来、前缀丢了),按这批数据补规则后 98.2%,所以这是开发集数字;真人留出集已备好提示和录音页,还没录。
- 延迟(CPU,不含模型第一句):端点等待 622 ms(按住说话为 0)+ 整句识别 471–545 ms + TTS 首包 624 ms(macOS
say)。CosyVoice 2 在 CPU 上做不到实时(首包 21 秒、RTF 约 5),要 GPU。 - 真人语音检查(公开语料,只测不调):Paraformer 两遍在 AISHELL-1 测试集上 CER 2.1%、FLEURS 7.6%,真人读的金额经 ITN 19/19 全对;但真人在句中停得久,600 ms 端点截断了 13.7%(AISHELL-1)到 53.3%(FLEURS)的句子,合成语音上只有 0.2%。
- 换一个没参与调规则的音色(CosyVoice),订单号先是 55.6%(它把“S O”念成“so”,识别器写“搜”),补上后 94.4%;Whisper 把热词放进提示反而让 CER 从 3.0% 升到 43.8%。
请如实理解这些数字:它们来自离线确定性模型和虚构数据,只能证明“流程按设计工作”(工具调对了、审批触发了、注入被挡住了),不代表真实模型的回答质量。换成真实模型后请重新运行评测,方法见 docs/EVALUATION.md。
答案缓存基准(python -m app.evals.cache_bench):评测集里 29 个知识类问题各问两遍,第二遍 23 个命中缓存,token 从 32,850 降到 6,676;没命中的 6 个是 5 条应拒答的问题(拒答按设计不缓存)和 1 条会建工单的报修问题(用了业务工具,本来就不缓存)。离线模型几乎瞬间返回,所以这里不报延迟收益;接入真实模型后,省下的是真实的首字延迟和 token 账单。
规模测试(GitHub Actions 4 vCPU,离线模型,PostgreSQL,4 个 worker,压测客户端和服务同机):并发 16 时 113 RPS、p95 181ms、p99 265ms;并发 64 时 87 RPS、p95 1.05 秒、p99 1.28 秒,全程 0 错误;4 核在并发 16 左右饱和,之后只是排队。Kubernetes 上按副本扩容(deploy/k8s 的 HPA)。没有真实流量,这不是线上容量,见 PERFORMANCE。
性能基线(python -m app.evals.load_bench,离线模型):本机(Apple M1 Max、单进程、SQLite WAL)并发 1 时约 160 RPS,其中分词缓存贡献了 11%–15%;GitHub Actions 上同一次运行内对比:PostgreSQL 改为批量写链路后,单 worker 吞吐提升 39%–47%;并发 16 时 4 workers 达到 129.7 RPS、p99 193ms。检索改为倒排表加向量矩阵后,2 万片段时单次检索从 391ms 降到 10ms(排序结果与逐个扫描一致)。每一步的数据、没有采用的方案和仍未解释的现象见 docs/PERFORMANCE.md。测的是模型以外的平台开销,不代表接入真实模型后的线上表现。接一个每次 6 秒的假模型做突发压测(--model-latency 6):改动前 60 个同时到达就有 15 个等 30 秒后 500、健康检查卡 30 秒;现在每进程最多同时运行 64 个回答,60 个全部答完(p50 6.2 秒),120 个时多出的 56 个立即 503 + Retry-After,流式接口同样处理;可选的 overlay 让 HPA 按在途运行数扩容(N-158)。名额按租户和用户分配:一个租户最多占 75%,一个顾客最多同时 4 个回答(超出 429);Leon优选 的顾客同时发 80 条时,1 秒后 Iris云 的 Noah 3 条全部答完(改动前同样的测试 3 条里 1 条 503)(N-181)。飞书事件也取同一份名额,满了在库里排队等名额(不能回 503,飞书已经确认收到):名额 8 时 60 条飞书事件改动前同时跑 48 个、loopdesk_inflight_runs 显示 0、确认 60 条事件花了 8.25 秒(飞书要求 3 秒内),现在最多同时 6 个(租户份额)、确认 0.26 秒、60 条全部答完(N-189)。
backend/
app/
agent/ Agent 循环、工具注册与业务工具、证据校验器、链路追踪
integrations/ 订单与支付系统网关:演示表,或按租户接商家自己的 HTTP 接口
rag/ 解析与切分、Embedding、BM25 + 向量 + RRF 混合检索、入库
llm/ OpenAI 兼容 / Claude / 离线模型适配器、模型路由
guardrails/ PII 脱敏、提示注入检测
flywheel/ 反馈归因、知识补丁、评测用例生成
evals/ 沙箱评测运行器、CLI 门禁
channels/ 飞书、企业微信
api/ REST + SSE 接口
ops/ 运维任务:渠道补偿、中断运行收尾、退款对账、数据保留、审计补签与锚点核对、上线前的模型自检、
迁移任务与等待、冒烟检查、演示录制、发布前的凭证扫描
mcp_server.py MCP 服务
evals/ 评测集(JSONL)
seed_data/ 演示知识库(Markdown)
tests/ pytest
frontend/src/ React 控制台(对话、审批、知识库、链路、飞轮、评测、看板)
deploy/k8s/ Kubernetes 清单(kustomize:base 和 kind overlay)
demo/real-model/ 在线演示用的真实模型录制(由 demo-record.yml 生成)
docs/ 架构、评测、部署、性能、挑战与风险文档
Python 3.12 · FastAPI · SQLAlchemy 2 · SQLite / PostgreSQL · jieba · httpx · PyJWT · pytest · React 19 · TypeScript · Vite · Docker · GitHub Actions · MCP
- 向量库切换到 pgvector / Milvus,支持十万级片段
- 异步化 Agent 循环(async httpx + async SQLAlchemy),并支持多副本部署(对话限流改用 Redis;登录限流和飞书事件去重已放进数据库,多 worker 共享)
- 评测集扩展到 200 条以上,接入真实模型做 A/B 对比
- 多 Agent:把售后和技术支持拆成两个专职 Agent,由路由 Agent 分派
- 飞书加密事件(AES)与企业微信应用消息回调
本项目为个人学习与求职作品。仓库中的企业名称、订单、客户和对话全部为虚构数据,不代表任何真实企业的政策。















