AI 实战记录(一)

3 阅读12分钟

给现有 SaaS 系统加 AI 能力,我踩过的坑和做对的选择

SaaS 管理系统,40+ 个业务 Skill,从零到 MVP 的架构决策全记录。 这篇文章不讲模型调参,不讲 prompt engineering 技巧,只讲一个真实业务系统接入 AI 时,架构层面怎么想、怎么选、为什么这么选


一、背景:不是做一个 AI 产品,是给现有系统加 AI

先说场景。我们有一套跑了几年的 SaaS 系统(Java 后端微服务 + 多端),涵盖会员、商品、订单、库存、服务、营销活动、报表、支付等八个模块。现在要做的事情是:让店员可以用自然语言操作这个系统

比如店员说「帮张三充值 200」,AI 解析意图,找到客户,确认金额,调用现有的充值接口完成操作。

这件事拆开来看,核心问题只有一个:AI 在这个系统里到底扮演什么角色?


二、第一个决策:AI 是系统的放大器,不是系统本身

这是整个项目最重要的一个认知,我想放在最前面说。

做 AI 应用的时候,很容易掉进一个陷阱:把所有东西都交给模型,让模型来决定做什么、怎么做、输出什么。模型能力越强,你越想多塞给它。但这样做有一个致命问题——你的产品稳定性和模型能力绑死了

模型本质上是一个推测器。它可以猜得很准,但它终究是在猜。

传统业务逻辑是选择题:A 条件下必须输出 B。AI 时代的产品更像简答题:A 条件下可能输出 B、C、D,它们不一定是错的,但你需要确保无论输出什么,都在你可控的范围内

所以我的原则是:AI 产品 = 产品结构 + AI 能力,而不是 AI 系统

具体到这个项目:

  • 意图识别交给 AI(它擅长理解自然语言)
  • 参数校验、权限检查、执行调用、结果格式化全部由确定性代码控制(不能猜)
  • 自由聊天交给 AI(本来就是开放域)
  • 但 Skill 执行结果严禁用 LLM 转述,避免 AI 编造数据(「张三余额 500 元」必须来自 API 返回值,不能是模型总结)

这样做的好处是:你换模型、模型升级、模型降级,都只影响「理解得准不准」「聊天好不好」,不影响「操作对不对」。DeepSeek 换成 OpenAI,或者通义千问从 plus 降到 turbo,执行层面一行代码不用改。


三、第二个决策:BFF 层还是改现有后端?

既然要调用现有系统的 API,有两种做法:

方案 A:直接在现有 Java 后端上加 AI 模块。 优点是能直接操作数据库,SQL 组合灵活,可以做到不受现有 API 限制的查询和操作。但问题也很明显:对后端代码得足够熟悉(改错了可能影响线上业务),Java 生态做 AI 相关开发不够顺手(LLM SDK、向量检索、异步流式等),而且 AI 相关的功能(会话管理、意图识别、埋点分析)和业务后端的关注点完全不同,混在一起后期维护是个灾难。

方案 B:新建一个 Python + FastAPI 的 BFF 层,透传 token 调用现有 API。 侵入性为零,现有后端一行不改。Python 做 LLM 调用、流式输出、异步编排都是主场。缺点是完全依赖现有 API——有些数据后端有但没暴露接口,你就拿不到,得回头找后端加字段。

我选了 B。核心理由:

  1. 风险隔离。AI 这边怎么折腾都不影响现有系统运行。
  2. AI 方向确定要独立迭代。后面要加向量搜索、加语义缓存、加 Agent 编排,这些东西放在 Java 微服务里做不伦不类。
  3. 即使要补接口,也只是简单增删改查,不需要动业务核心逻辑。

四、架构设计:把流程拆成可追溯的节点

确定了选型,下一步是架构。一个自然语言请求从进来到出去,需要经过什么?

最直觉的做法是:把用户的话 + 所有 Skill 描述丢给 LLM,让它告诉你调哪个接口、传什么参数,然后执行,最后让 LLM 把结果总结成一句话返回。一个 LLM 调用搞定一切。

这在 demo 里跑得很顺,但在生产环境里是灾难。

原因很简单:出了问题你不知道是哪一步出的。是意图理解错了?参数提取错了?权限没过?接口报错了?还是结果总结的时候 LLM 编造了数据?黑盒一整坨,排查基本靠猜。

所以我把整个流程拆成了管道(Pipeline),每个阶段独立、可审计、可追溯: 用户输入   ↓ [Parse] 意图识别:用户想干什么?调哪个 Skill?参数是什么?   ↓ [Validate] 参数校验:必填参数齐了吗?类型对吗?有权限吗?   ↓ [Consistency] 一致性:如果在多轮对话中,当前意图和之前追问的是同一件事吗?   ↓ [Decision] 决策路由:参数缺了追问、需要确认就弹卡片、都齐了就执行、闲聊就聊天   ↓ [Execute] 执行:调用后端 API,写操作落库做补偿记录   ↓ [Verify] 验证:写操作执行后回查确认是否生效   ↓ [Output] 输出:格式化结果、发卡片或流式文本 text每个阶段的输入输出都记录到审计日志(trace_id 贯穿全链路),任何一步出了问题,拿 trace_id 就能定位到具体是哪个阶段、输入了什么、输出了什么、耗时多少。

白盒化是这个架构最重要的设计目标。 LLM 做它擅长的事(理解语言),但每一步的决策逻辑都是代码写死的 if/else,不是模型在「想」。


五、意图识别的演进:为什么 MVP 阶段不用 Embedding?

项目初期 Skill 不多的时候,意图识别很简单:把所有 Skill 的名字、描述、参数列表拼成一段文本,和用户的话一起发给 LLM,让它返回 JSON 告诉你 intent 是什么、参数是什么。

但当 Skill 增长到 50 个时,问题来了:

  • 每次请求带上全部 Skill 描述大约 2000 token,不管用户说的是「帮我查库存」还是「今天天气真好」
  • 如果用户在闲聊,这 2000 token 就是纯浪费
  • 如果用户在继续上一步操作(补充参数),也不需要重新做全量意图识别

这时候有两个优化方向:

方案一:域 Hint 预筛选(关键词 + 规则)

把 50 个 Skill 按用户心智模型分成 7 个语义域(会员、商品、库存、订单、服务、营销、报表)。每个域定义两层关键词:

  • 锚点词:高置信度,出现就命中。比如「充值」「余额」→ 会员域,「上架」「改价」→ 商品域。
  • 上下文词:有歧义,需要组合判断。比如「多少钱」可能是商品价格也可能是会员余额,结合其他词判断。

命中某个域后,只把该域的 5-10 个 Skill 发给 LLM,而不是全部 50 个。token 从 2000 降到 300-800,延迟从 3-4 秒降到 0.5-1.2 秒。

没命中任何域?走 fallback,还是传全量,但用压缩格式(单行紧凑描述,token 再减 50%)。

方案二:Embedding 向量匹配

把所有 Skill 描述向量化,用户输入也向量化,算相似度取 Top-K,直接匹配。优点:不用维护关键词表,扩展性好,延迟极低(本地模型几十毫秒)。缺点:黑盒,匹配错了不好调试,需要花时间选模型和调阈值。

我选了方案一,原因很实际:

  1. MVP 阶段要的是能跑、可控、可调试,不是最优雅。 关键词表虽然要手动维护,但每个命中/未命中都是确定性的,日志一看就知道为什么走了这个域。Embedding 命中了但命中错了,你很难快速排查。

  2. 关键词覆盖率比想象中高。 业务场景下,用户说话带业务词的概率非常高(「充值」「查库存」「出库」「订单」),80% 的请求锚点词直接命中。

  3. Embedding 的价值在规模更大的时候才体现。 50 个 Skill 用关键词能管,200 个 Skill 关键词就不现实了。我的规划是 Skill 到 60-80 个时切换到 Embedding,但前期积累的关键词命中/未命中日志,刚好可以作为 Embedding 模型的评估基准。

  4. 还加了一个自动修正机制:如果域 Hint 没命中但 fallback 全量命中了,说明关键词表有盲区,这些 case 自动记录下来。一周扫一次日志就能补上。长期来看关键词表会越来越准,而这些数据对后续切 Embedding 也是重要的训练/评估资源。

一句话:选方案不是选最先进的,是选当前阶段最匹配的。 能用规则解决的问题,不需要上模型。


六、Skill 的两种类型,复杂度完全不同

所有 Skill 分两类:查询类操作类(写操作)

查询类很简单:Parse 拿到意图和参数 → Execute 调 API → Output 格式化展示。比如「张三还有多少钱」→ 查客户余额 → 返回「张三当前余额 500 元」。

操作类复杂得多,因为要处理多轮交互不可逆性

以「给张三充值 300 元」为例,实际流程是:

  1. Parse:识别意图 = 充值,提取参数 keyword=张三、amount=300
  2. Decision:这是写操作,需要确认 → 调 Skill 的 prepare_confirm()
  3. prepare_confirm 内部:按「张三」搜索客户 → 如果搜到多个张三,返回选择卡片让用户选 → 选定后拉取会员卡信息 → 生成确认卡片(「确认为张三充值 300 元到金卡?」)
  4. 用户确认:下一轮请求进来,session_state=confirming → Parse 检测到「确认」→ Execute 执行充值
  5. Verify:充值后回查余额,确认确实加上了
  6. Output:返回「已成功为张三充值 300 元」+ 撤回按钮

注意:整个过程里只有一个意图(充值),不是「先调查询 Skill 查客户,再调充值 Skill」。查客户是充值 Skill 内部的逻辑,对管道来说始终是同一个 Skill 在走流程。

如果用户只说了「给张三充值」没说金额怎么办?参数校验不通过 → Decision 走 ask 路由 → 追问「请输入充值金额」→ 进入 slot_collecting 状态 → 用户回复「300」→ 参数补齐 → 继续上面的流程。

这就引出了状态机

状态含义怎么来的
idle待命默认状态,啥都没挂着
slot_collecting在追问参数用户说的不全,系统在等补充
confirming在等确认确认卡片已弹出,等用户点确认或取消

状态不是存在数据库里的,而是推导出来的:有待确认对象 → confirming,有槽位收集对象 → slot_collecting,都没有 → idle。这样不需要额外维护状态字段,也不会出现状态和实际数据不一致的问题。


七、写操作的安全网:确认 + 补偿 + 撤回

操作类 Skill 要格外小心,因为一旦执行就可能造成实际影响(充了钱、改了价、出了库)。所以设计了三层安全网:

  1. 二次确认:所有写操作必须弹确认卡片,用户明确点「确认」才执行。不存在「AI 自动帮你充值了」这种情况。
  2. 补偿记录:每个写操作执行前写入一条 processing 记录,执行后更新为 success/failed/needs_review。即使系统崩溃,也能从记录里知道哪些操作执行了一半。
  3. 撤回机制:所有写操作支持 24 小时内撤回。执行成功时保存撤回快照(旧值),撤回就是把旧值写回去或调反向接口。仅通过按钮触发,不支持自然语言触发(避免误撤回)。

八、结果输出:规则格式化,不要让 LLM 总结业务数据

这是另一个重要决策。早期做的时候,Skill 执行完拿到 API 返回的 JSON,直接丢给 LLM 让它「用自然语言总结一下」,看起来很优雅。

但很快发现问题:

  • LLM 偶尔会编造不存在的字段值
  • 数字精度可能被四舍五入
  • 枚举值翻译不一致(同一个 status=1 这次说「已完成」下次说「已结束」)

所以改成了确定性格式化:每个 Skill 自己写 format_result() 方法,所有枚举翻译、字段映射、文案模板都在 Python 代码里写死。同样的输入永远产出同样的输出,LLM 不参与。

对于结构化数据(订单列表、客户详情、仪表盘指标),直接发结构化的 data_card 事件给前端,前端按类型渲染对应卡片。不做文本转换,数据原样展示,零幻觉。

LLM 在输出环节唯一的用途是:自由聊天。用户说「今天天气真好」,没有命中任何 Skill,那就让 DeepSeek 陪聊。这个场景本来就是开放域,模型发挥就对了。


九、总结:几个可能对你有用的原则

  1. AI 是放大器,不是基座。 你的产品结构必须在没有 AI 的情况下也能说清楚。AI 让它更快、更自然、更智能,但不应该是「没有 AI 就跑不了」。

  2. 能用规则解决的,不用模型。 参数校验、权限检查、结果格式化、状态流转,这些有确定性答案的事情交给代码。模型只做需要「理解」的事情。

  3. 节点化、白盒化、可追溯。 把流程拆成独立的阶段,每一步有明确的输入输出和审计日志。出了问题拿 trace_id 一查就知道卡在哪。

  4. 选方案不是选最先进的,是选当前阶段最匹配的。 MVP 用关键词能跑就先跑,规模上来了再换 Embedding,数据积累了再上本地模型。但架构设计时要预留升级路径,别把自己锁死。

  5. 对写操作保持敬畏。 查询出错最多给用户看错数据,写操作出错是真金白银。确认、补偿、撤回,三层都不能省。


以上是一个 SaaS 系统接入 AI 的实战记录。项目仍在迭代中,后续如果 Embedding 替换域 Hint、向量语义搜索上线,会继续分享。