给传统 SaaS 增加 AI 能力:3个真实落地场景
上一篇《RAG 检索优化实战》把检索 Hit Rate 从 67% 拉到了 92%,知识库算是能打了。
但知识库本身不产生价值。它得长在业务系统里,用户才会用。
这半年我们给三家传统 SaaS 产品接了 AI 能力:一家客服工单系统、一家销售管理 CRM、一家数据分析平台。三个场景难度递增,坑也一个比一个深。
这篇文章把整个过程拆开:每个场景为什么选它、架构怎么搭、代码怎么写、上线后数据什么样、踩了哪些坑。没有概念图,全是落地细节。
核心就 3 个场景,难度递增:
- 智能客服------把 FAQ 和知识库变成自动应答,先解决高频重复问题
- 业务助手------把 AI 嵌进用户每天用的工作流,生成草稿等人确认
- ChatBI------让用户用自然语言直接查数据,生成 SQL 并自检
每个场景都按同一套标准选出来的,最后还会给一张对比表和上线顺序。
先回答一个问题:你的 SaaS 该不该加 AI
不是所有产品都适合现在加 AI。先搞清楚三条路,再决定走哪条。
第一种:功能层叠加。 在现有产品上面加一个 AI 层,比如侧边栏助手、智能搜索、自动摘要。底层业务逻辑不动,AI 不参与流程决策,只是人的工具。关掉 AI 层,产品照常跑。
第二种:工作流嵌入。 AI 参与业务流程的某个环节,比如自动生成跟进记录、智能分派工单、辅助报价。AI 开始接触业务数据,需要权限控制和写操作管理。
第三种:原生化。 用 Agent 重新实现原有业务,交互方式都变了。比如把"填表单"变成"对话式完成任务"。这条路改的是产品本身,周期长、风险高,不是本文讨论的范围。
我们接的三个场景都属于前两种。为什么?因为传统 SaaS 的存量用户不会因为 AI 重新学一遍产品,你得让他们在原来的操作习惯里"顺便"用上 AI。
场景选择的 4 条判断标准
帮客户选场景时,我们只用 4 条标准,全部满足才做:
- 高频。 用户每周甚至每天都会遇到的问题。低频场景做出来没人用,纯浪费。
- 数据现成。 业务系统里已经有数据,不需要用户额外维护。要用户先建知识库再享受 AI,基本会黄。
- 错误可容忍。 AI 答错或做错的后果有限,且有兜底。涉及资金、合同、诊断结论的场景,先靠边站。
- 不碰核心决策。 AI 只做辅助,最终决定权在人。上来就要 AI 自动执行关键操作的产品,风险自己扛。
决策树
这个场景值得做吗?
│
├─ 用户问题/操作高频吗?
│ ├─ 否 → 不做。先找高频场景
│ └─ 是 ↓
├─ 数据现成吗(不用用户额外维护)?
│ ├─ 否 → 先解决数据问题,否则做了也是空壳
│ └─ 是 ↓
├─ 错误后果可控吗(有补救手段)?
│ ├─ 否 → 只做辅助建议,不做自动执行
│ └─ 是 ↓
├─ 会触碰核心业务决策吗?
│ ├─ 会 → 加人工确认层,AI 只出草稿/建议
│ └─ 不会 → 可以做,从最小闭环开始
这套决策树在后面三个场景里反复用到。现在开始讲落地。
拿这套标准去筛,大部分"AI 功能"需求当场就废了。有位客户想做个"AI 自动跟进客户",听起来很酷,但第 3 条就不满足:AI 给客户发错消息,损失的是真实客户关系,而且没有补救机制。我们建议改成"AI 生成跟进草稿,销售确认后发送",客户接受了,效果反而更好。
场景一:智能客服,把 FAQ 和知识库变成自动应答
为什么先做它
第一家客户是客服工单系统,用户的典型问题长这样:"怎么导出报表""API 调用限制是多少""套餐怎么升级"。这些问题有 FAQ、有产品文档,答案都在里面,但客服团队每天要手动回复几十遍。
选它做第一个场景有三个理由:
不懂文档解析和分块策略怎么搭?先看这篇:《RAG 核心技术实战:文档解析、分块策略与检索 Pipeline》,本篇直接复用那套底座。
- 复用现成底座。 系列前两篇做的文档解析、分块、检索、重排,直接搬过来,不用重写。
- 风险低。 客服答错不会造成资金损失,最多用户不满意,有人工接住。
- 见效快。 客户看得见的快赢,后面推第二个场景就顺了。
判断标准过一遍:高频(满足)、数据现成(FAQ 和文档都有)、错误可容忍(有人工接住)、不碰核心决策(不满足第 4 条的自动执行,只做回答)。可以做。
架构:FAQ 精确匹配优先,RAG 兜底,人工接住
很多人以为智能客服就是"用户问问题 → RAG 检索 → LLM 生成答案"。太天真了。
真实流量里,大部分问题是重复的、有标准答案的。用 RAG 走一遍,慢、贵、还可能答歪。正确做法是三层分流:
用户消息
│
├─ ① FAQ 精确匹配(阈值 0.92)→ 命中?→ 直接返回预设答案(毫秒级,零成本)
│
├─ ② RAG 检索 + 生成(复用第 2、3 篇的 Pipeline)
│ └─ 置信度低?→ 转人工
│
└─ ③ 人工接住:转人工时带上对话摘要和 AI 的初步判断
第一层 FAQ 匹配不是玄学。FAQ 数量一般几百条,用向量相似度匹配,命中阈值设高点,宁可漏过也不要误命中。命中就直接返回标准答案,不走 LLM。
第二层才轮到 RAG。检索置信度低于阈值就转人工,不要硬答。
第三层是很多团队忽略的:转人工不是简单地把会话丢给客服。AI 先把对话总结好,客服接手时不用从头读,效率完全不一样。
核心代码
python
# ai_support.py - 智能客服三层分流
from dataclasses import dataclass
@dataclass
class SupportConfig:
faq_threshold: float = 0.92 # FAQ 命中阈值,宁可漏不可错
rag_threshold: float = 0.60 # RAG 置信度下限,低于则转人工
model: str = "qwen-plus" # 生成模型,按成本可换小模型
class AISupport:
def __init__(self, faq_store, rag_retriever, llm, config: SupportConfig):
self.faq_store = faq_store # FAQ 向量索引,问题->标准答案
self.rag = rag_retriever # 第 2、3 篇的检索 Pipeline
self.llm = llm
self.config = config
def handle(self, message: str, user_id: str, history: list) -> dict:
"""处理一条用户消息,返回回答或转人工指令"""
# ① FAQ 精确匹配优先:快、准、零成本
faq = self.faq_store.match(message, threshold=self.config.faq_threshold)
if faq:
return {"action": "answer", "reply": faq.answer,
"source": "faq", "confidence": faq.score}
# ② RAG 检索 + 生成
result = self.rag.retrieve_with_score(message) # 返回 (chunks, score)
if result.score < self.config.rag_threshold:
return self._escalate(message, user_id, history,
reason=f"检索置信度 {result.score:.2f} 低于阈值")
answer = self.llm.generate(
system="你是该 SaaS 产品的客服助手。只根据提供的文档回答,"
"文档中没有的信息直接说不知道,不要编造。",
context=result.chunks,
question=message,
history=history[-6:], # 只带最近 3 轮对话,省 token
)
return {"action": "answer", "reply": answer,
"source": "rag", "confidence": result.score,
"citations": [c.doc_id for c in result.chunks[:3]]}
def _escalate(self, message, user_id, history, reason) -> dict:
"""转人工:带上 AI 的摘要和判断,客服不用从头读对话"""
summary = self.llm.summarize(
"用两句话总结这段对话,标注用户的核心诉求:",
history + [{"role": "user", "content": message}],
)
return {"action": "escalate", "summary": summary, "reason": reason}
两个细节值得说。
FAQ 阈值设 0.92 是故意的。 误命中比漏命中可怕。漏了最多多花一次 RAG 调用,误命中了直接给用户一个驴唇不对马嘴的标准答案,信任感瞬间归零。我们上线初期故意调高阈值,宁可让更多问题走 RAG,也不让 FAQ 乱答。
转人工带摘要。 客服接到会话时先看到"用户想升级套餐,但企业版报价需要人工确认"这种摘要,而不是 20 条原始消息。第一次上线这个功能,客服团队主动过来问"这个摘要能不能再详细点"。需求是不是被接住了,看这个反应就够了。
上线节奏:灰度三步走
我们没有一步到位。按流量灰度,每步观察 24 小时:
| 阶段 | 流量 | 动作 | 观察指标 |
|---|---|---|---|
| 第 1 周 | 10% | AI 先答,转人工照旧 | 自动解决率、转人工率、客诉 |
| 第 2 周 | 50% | 扩大流量 | 同上 + 未匹配问题清单 |
| 第 3 周 | 100% | 全量 | 周度报告:未匹配问题、满意度、持续补 FAQ |
每天人工抽 10 条 AI 回答过的对话核对质量,发现问题当天修。不是"上线了就不管了",前两周我们基本每天都要调 FAQ 和补检索词。
上线一个月后的数据(灰度期间统计):
| 指标 | 上线前 | 一个月后 |
|---|---|---|
| 首响时间 | 平均几分钟 | 秒级(FAQ 命中) |
| 自动解决率 | 0 | 六成左右(AI 处理后用户未再追问/未转人工) |
| 转人工率 | 100% | 三成左右 |
| 客服重复回答量 | 每天几十次 | 明显下降 |
需要说明的是,这些数字是我们这个场景的灰度数据,不同产品差异会很大。客服问答类产品的"自动解决率"和问题复杂度强相关,别拿别人的数字当自己的 KPI。
踩坑:AI 答错比不答更伤信任
最大的坑是"硬答"。
一开始 RAG 置信度低于阈值时,我们也让 LLM 硬着头皮答。结果用户问了个文档里没有的问题,AI 编了一个像模像样的答案,用户信了,照着操作,系统报错。用户回来投诉,比不回答更生气。
从此我们立了一条规矩:置信度不够就转人工,宁可让用户等,不能让他信错。 这条规矩后来救了第二个场景的命。
另一个坑是"自动率"这个指标。
有同事提议看"AI 自动回复率",说这个数字高就说明效果好。我们算了算:把简单问题全自动、难问题全转人工,自动率能刷到很高,但用户感知的提升有限。真正有用的指标是自动解决率:AI 回答后,用户没有追问、没有转人工、没有差评。这个指标才反映"问题真的被解决了"。
场景二:业务助手,把 AI 嵌进用户每天用的工作流
为什么做它
客服场景只覆盖了产品外围。用户天天用的核心操作,写跟进记录、总结客户、起草回复,这些 AI 还没碰。
第二家客户是销售管理 CRM。销售每天最烦的事不是"没信息",是"信息要自己整理":打开客户的沟通历史,翻十几条记录,提炼重点,再写跟进。一天几个客户下来,半天就没了。
业务助手的做法很朴素:在客户详情页加一个按钮,点击后:
- 总结客户:拉取该客户的工单、邮件、通话记录,生成一页摘要
- 生成跟进草稿:结合客户阶段和最近动态,生成跟进记录草稿,销售改完再保存
- 起草回复:针对客户最近的邮件/消息,生成回复草稿
三个动作都不复杂,但都嵌在真实工作流里,销售每天用。这才是"给 SaaS 加 AI"和"做了一个 AI 功能"的区别。
标准过一遍:高频(每天用)、数据现成(CRM 里全有)、错误可容忍(草稿,人确认后才生效)、不碰核心决策(AI 只出草稿,人拍板)。可以做。
架构:业务 API + 权限校验 + 写操作二次确认
客服场景只需要读文档。业务助手要碰业务数据,架构上多了三样东西:
markdown
前端按钮 → 后端助手服务
│
├─ ① 工具注册表:每个工具声明参数、需要的权限、是否写操作
├─ ② 权限校验:服务端统一校验,注入用户上下文(租户、角色、数据范围)
└─ ③ 写操作一律返回草稿,用户确认后才落库
工具注册表是核心。LLM 不直接调业务 API,它只决定"调用哪个工具、传什么参数",真正执行的是后端代码。每个工具声明自己的权限要求,服务端统一校验。
权限校验必须在服务端做,不能在 prompt 里做。 这个后面细说,是我们踩过的最深的坑。
写操作二次确认。 AI 生成的内容一律以草稿形式返回前端,用户点"确认保存"才写入。这个设计看起来多了一步,但挡住了几乎所有 AI 事故。
核心代码
python
# assistant.py - 业务助手:工具注册 + 权限校验 + 草稿机制
from functools import wraps
class ToolRegistry:
"""工具注册表:LLM 只能看到这里注册的工具"""
def __init__(self):
self._tools = {}
def register(self, name, requires_perm=None, is_write=False, description=""):
"""注册工具。requires_perm 是权限标识,is_write 标记写操作"""
def decorator(fn):
fn.meta = {
"name": name,
"requires_perm": requires_perm,
"is_write": is_write,
"description": description,
}
self._tools[name] = fn
return fn
return decorator
def list_for_llm(self, user_perms: set) -> list:
"""只返回用户有权限的工具给 LLM,避免 LLM 幻觉出没权限的工具"""
return [
{"name": t.meta["name"], "description": t.meta["description"],
"is_write": t.meta["is_write"]}
for t in self._tools.values()
if not t.meta["requires_perm"] or t.meta["requires_perm"] in user_perms
]
def call(self, name, user_ctx, **kwargs):
"""执行工具:权限校验 + 写操作标记"""
tool = self._tools.get(name)
if not tool:
return {"ok": False, "error": f"unknown tool: {name}"}
if tool.meta["requires_perm"] and \
tool.meta["requires_perm"] not in user_ctx.permissions:
return {"ok": False, "error": "permission denied"}
result = tool(user_ctx=user_ctx, **kwargs)
if tool.meta["is_write"]:
# 写操作:返回草稿,等用户确认,绝不直接落库
return {"ok": True, "draft": result, "confirm_required": True}
return {"ok": True, "data": result}
registry = ToolRegistry()
@registry.register("summarize_customer", requires_perm="customer:read",
description="总结指定客户的沟通历史,返回一页摘要")
def summarize_customer(user_ctx, customer_id: str):
# 关键:数据查询必须带用户上下文,服务端强制过滤
records = db.query_interactions(customer_id, tenant_id=user_ctx.tenant_id,
scope=user_ctx.data_scope)
return llm.summarize("把以下沟通记录压缩成要点,按时间排序:", records)
@registry.register("draft_followup", requires_perm="customer:write",
is_write=True,
description="根据客户最近动态生成跟进记录草稿")
def draft_followup(user_ctx, customer_id: str, focus: str = ""):
history = db.query_interactions(customer_id, tenant_id=user_ctx.tenant_id,
scope=user_ctx.data_scope)
return llm.generate(
"你是销售助理。根据客户沟通历史,生成一条跟进记录草稿,"
f"重点:{focus}。语气专业,不要编造历史中不存在的细节。",
history,
)
三个设计点,每个背后都有一次事故:
LLM 只看到有权限的工具。 list_for_llm 按用户权限过滤工具列表。LLM 根本不知道有 draft_followup 这个工具,就不会去调它。这比"让 LLM 调了再校验"安全得多,问题在源头就被挡住了。
服务端强制过滤数据。 summarize_customer 查询时强制带 tenant_id 和 data_scope。这不是可选参数,是服务端拼进去的,前端和 LLM 都传不了。数据隔离只能靠服务端,不能靠 LLM 自觉。
写操作返回草稿。 所有 is_write=True 的工具,结果以 draft 形式返回,前端展示确认按钮。用户点确认,后端才执行真正的写入。AI 永远没有"直接改数据"的能力。
踩坑:权限是命门,写操作必须人确认
坑一:权限校验放错了地方。
第一版我们把权限检查写在 prompt 里:"你只能访问当前用户有权限的数据。"看起来没问题,直到一个测试账号问到了别的团队的客户数据。
原因不复杂:助手服务端调用业务 API 时用的是服务端服务账号 的权限,而不是发起请求的用户的权限。LLM 生成的工具调用,传什么参数就查什么数据,prompt 里的"你只能......"根本拦不住。
修复方案就是上面的代码:工具查询强制拼 user_ctx(租户 + 数据范围),服务端统一校验,权限在代码里,不在 prompt 里。
这件事之后我们定了一条铁律:AI 功能的权限模型和原产品完全一致,不允许有第二套。 AI 只是给用户多了一个入口,不代表用户多了权限。
坑二:让 AI 直接写数据。
有段时间我们让 draft_followup 直接落库,理由是"草稿还要人确认,多此一举"。结果一次测试中,AI 把客户的销售阶段从"已报价"改成了"已签约"。原因很简单,客户的历史消息里有一句"我们下周签约"。
信息没错,但"下周签约"和"已签约"是两个状态。AI 分不清预测和事实。从此所有写操作一律走草稿确认流程,一条例外都没有。
坑三:token 成本开始显形。
客服场景的成本可以忽略,因为 FAQ 命中率六成,真正走 LLM 的不多。业务助手不一样:每个销售每天用十几二十次,每次调用要带客户历史,几千 token 起步。第一个月账单出来,客户问了一句"这个费用是怎么回事"。
我们的对策:
- 小任务用小模型。 总结、草稿这类任务,换成参数小一档的模型,质量几乎无差,成本低一截。只有复杂任务才用大模型。
- 缓存总结。 客户历史摘要按客户 ID 缓存,当天内重复生成直接返回缓存,不重复调 LLM。
- 截断历史。 只带最近 N 条记录,不是全量历史。全量历史又贵又容易让模型抓不住重点。
成本要省,得在设计阶段就考虑。后面 ChatBI 场景就是按这个思路做的。
场景三:ChatBI,让用户用自然语言查数据
为什么放最后
第三个场景是数据分析平台。用户经常想查"上季度华东区退货率最高的三个品类",但他们不会写 SQL,也不想学。传统做法是提工单,数据团队排期,两天后收到一个 Excel。
ChatBI 的目标:用户输入自然语言,系统生成 SQL、执行、返回结果,再配一段人话解读。
为什么放最后?三个原因:
- 要碰数据库。 前面两个场景碰的是文档和业务 API,这个直接碰原始数据,出错的代价完全不同。
- SQL 幻觉比文本幻觉危险。 文本答错,用户能看出来;SQL 查错,结果看起来是对的,用户根本不知道,这是最危险的情况。
- 它其实是 Agent 的雏形。 生成 SQL、执行、看结果、不行重来,这就是下一篇文章要讲的 Agent 的基本循环。先理解这一点,再回头看 Agent 会轻松很多。
拿标准过一遍:高频(提数需求确实高频)、数据现成(平台里全是数据)、错误可容忍(只读查询,不写库,但错误结果本身有风险)、不碰核心决策(AI 只查数,决策人看结果)。可以做,但要把错误风险压到最低。
架构:NL2SQL + 权限过滤 + 结果校验
sql
用户问题 → ① 意图确认(是不是查数需求?要不要写?)
→ ② 表/字段映射(只允许白名单内的表和字段)
→ ③ NL2SQL(LLM 生成 SQL,带约束)
→ ④ 权限注入(强制拼 tenant_id 过滤 + 列级权限裁剪)
→ ⑤ 安全执行(只读账号 + 超时 + LIMIT 上限)
→ ⑥ 结果自检(LLM 复核 SQL 和结果是否匹配问题)
→ ⑦ 生成解读
每一步都在压缩"出错的空间"。我逐个说:
① 意图确认。 不是所有问题都该走 SQL。"为什么上个月销售额下降"可能根本不需要查库,或者需要先澄清口径。先让 LLM 判断这是不是查数需求、是否涉及写入操作。写操作直接拒绝,ChatBI 只读。聚合还是明细这类细节,等 SQL 生成时再处理,意图确认阶段不用管。
② 白名单。 LLM 不知道数据库全貌,只给它表清单和字段清单。表名、字段名全部来自系统元数据,LLM 无法发明表。
③④ 权限。 用户只能查自己有权限的表和行。实现上是两件事:工具层只暴露有权限的表;SQL 生成后强制注入 WHERE tenant_id = ?(多租户)和行级过滤条件。注入是服务端做的,LLM 生成的 SQL 会被改写。
⑤ 安全执行。 只读数据库账号、查询超时、LIMIT 上限。防止"生成了一条全表扫描"拖垮数据库。
⑥ 结果自检。 SQL 执行完,让 LLM 复核一遍:"这个 SQL 和用户的问题匹配吗?结果里有明显可疑的数据吗?"不匹配就重生成,最多重试两次。这一步挡住了不少低级错误,比如 LLM 把"最近30天"写成"最近30年",自检时一看时间范围就发现了。
核心代码
python
# chatbi.py - 安全 NL2SQL:白名单 + 权限注入 + 结果自检
import re
class ChatBI:
def __init__(self, llm, schema, db, perm_checker):
self.llm = llm
self.schema = schema # 白名单表/字段元数据
self.db = db # 只读连接
self.perm_checker = perm_checker
def ask(self, question: str, user_ctx) -> dict:
# ① 意图确认:是不是查数需求
intent = self.llm.classify(
question,
labels=["data_query", "clarify", "write_request", "other"],
# 聚合/明细等细节不在意图层判断,交给下游 SQL 生成时处理
)
if intent == "write_request":
return {"error": "ChatBI 只支持查询,不支持写入。"}
if intent in ("clarify", "other"):
return {"error": "请把问题描述得更具体,比如加上时间范围和指标。"}
# ② 生成 SQL:只给白名单 schema,LLM 无法发明表
sql = self.llm.generate_sql(
question=question,
schema=self.schema.filter(user_ctx.permissions), # 列级权限裁剪
rules="只允许 SELECT;不允许子查询嵌套过深;"
"时间条件必须显式写出,禁止用隐含默认值。",
)
# ④ 权限注入:服务端强制加租户/数据范围条件
sql = self._inject_scope(sql, user_ctx)
if not self._validate(sql):
return {"error": "SQL 未通过安全检查,已拦截。"}
# ⑤ 安全执行:只读 + 超时 + LIMIT
sql = self._force_limit(sql, max_rows=500)
try:
result = self.db.query_readonly(sql, timeout_seconds=10)
except Exception as e:
return {"error": f"查询失败:{e}"}
# ⑥ 结果自检:LLM 复核 SQL 和问题是否匹配
check = self.llm.verify(
question=question, sql=sql, sample=result.head(10),
prompt="检查这个 SQL 是否回答了用户的问题,"
"时间范围和筛选条件是否正确。只回答 OK 或问题描述。",
)
if check != "OK":
return {"error": f"结果自检未通过:{check}。请换个说法再问一次。"}
# ⑦ 生成解读
summary = self.llm.explain(question=question, data=result.head(20))
return {"data": result.to_dict("records"), "summary": summary}
def _inject_scope(self, sql, user_ctx) -> str:
"""把租户过滤强制注入 WHERE 子句"""
scope_cond = f"tenant_id = {user_ctx.tenant_id}"
if user_ctx.data_scope:
scope_cond += f" AND {user_ctx.data_scope}"
if re.search(r"\bwhere\b", sql, re.IGNORECASE):
return re.sub(r"\bwhere\b", f"WHERE {scope_cond} AND",
sql, count=1, flags=re.IGNORECASE)
return f"{sql.rstrip(';')} WHERE {scope_cond}"
def _validate(self, sql) -> bool:
"""粗粒度安全校验:只读、无危险写法"""
sql_upper = sql.strip().upper()
# 禁止写操作关键词开头
dangerous = ("INSERT", "UPDATE", "DELETE", "DROP", "ALTER", "CREATE", "GRANT")
if any(sql_upper.startswith(kw) for kw in dangerous):
return False
# 禁止多条语句(分号分隔),允许末尾单个分号
if ";" in sql_upper.rstrip(";"):
return False
return True
def _force_limit(self, sql, max_rows: int) -> str:
if re.search(r"\blimit\b", sql, re.IGNORECASE):
return sql
return f"{sql.rstrip(';')} LIMIT {max_rows}"
这段代码值得细看的就两点。
权限注入是字符串级的,不是 prompt 级的。 _inject_scope 在服务端直接改写 SQL,把租户条件拼进 WHERE。LLM 生成的 SQL 再花哨,最后执行的都是被改写过的版本。这是 ChatBI 场景里最重要的安全防线。
自检挡住的是"看起来对"的错误。 文本错误用户能发现,SQL 错误用户发现不了。自检这一层专门对付"时间范围错了""筛错维度了"这类低级但隐蔽的错误。不是万能的,但能把错误率压下一个量级。
踩坑:SQL 幻觉和"离线评估≠线上效果"
坑一:时间范围幻觉。
测试集上 SQL 正确率很高,上线后第一个真实用户问题就把我们干懵了:"查一下我们最近的表现。"
LLM 生成 SQL 时没写时间条件。"最近的表现"这种模糊说法,模型默认了整表数据。结果把过去三年的数据全拉出来了,还配了一段一本正经的解读:"贵司近三年表现稳定。"用户当场质疑数据。
修复了两层:第一,生成规则里强制"时间条件必须显式写出,禁止隐含默认值",没有明确时间范围就反问用户;第二,结果自检专门检查时间范围。
坑二:LLM 会发明不存在的逻辑。
有一次用户问"华东区各品类的退货率",LLM 生成 SQL 时把订单表和商品表 join 错了,用了 order.product_id = product.order_id 这种不存在的关联。结果没报错------两个表的 ID 都是自增序列,数值恰好能对上,查询"成功"了,还返回了一堆看起来正常的数字。AI 甚至配了一段解读:"华东区家居类退货率偏高,建议关注物流时效。"
我们对着结果愣了半天。数据没报错、数量级对、结论也合理,直到拿一条已知订单手工核对,才发现关联键完全是错的。这类错误比幻觉难抓得多:幻觉往往是"看起来不像话",它却是"看起来太合理"。还有一次,LLM 在 SQL 里写了个 HAVING count(*) > 0,完全多余,但因为结果没错,自检也没拦下来。
我们的应对是高频问题缓存:把常见问题("月度销售额""退货率 TOP10")的 SQL 固化下来,用户问相似问题时直接用固化 SQL,不经过 LLM 生成。缓存命中率越高,出错空间越小,成本也越低。
坑三:离线评估不等于线上效果。
我们在 80 条测试集上把 SQL 正确率刷到很高,上线后照样被真实问法打穿:"哪个月最惨""卖得最差的是啥""环比跌了没"。这些口语化、缺主语、缺时间的问法,测试集里一条都没有。
解法很土但有效:线上 bad case 全部进评估集,每周回归一次。 用户每问出一个让系统翻车的说法,就把它加进测试集,下次优化必须保证它通过。评估集从 80 条涨到 200 多条,系统被真实问法"训练"得越来越稳。
三个场景横向对比与上线顺序
三个场景做完,回头整理一张表:
| 维度 | 智能客服 | 业务助手 | ChatBI |
|---|---|---|---|
| 碰什么数据 | 文档、FAQ | 业务记录(CRM 等) | 原始数据库 |
| 交互方式 | 对话式(新入口) | 嵌入现有工作流 | 对话式(新入口) |
| 读写能力 | 只读 | 读 + 草稿写 | 只读 |
| 权限复杂度 | 低 | 中(服务端强制过滤) | 高(SQL 级注入) |
| 开发周期 | 2-3 周 | 3-4 周 | 4-6 周 |
| 风险等级 | 低(有人工接住) | 中(写操作需确认) | 高(错误结果隐蔽) |
| 复用前 3 篇成果 | 直接复用 | 部分复用(检索) | 少量复用 |
推荐上线顺序:智能客服 → 业务助手 → ChatBI。 理由不是按难度排的,是按"信任积累"排的:
- 客服先上,风险低、见效快,客户和用户先建立"AI 靠谱"的认知。
- 助手跟上,让 AI 从回答者变成协作者,用户开始把 AI 当工具用。
- ChatBI 最后,此时团队已经积累了权限体系、评估集、灰度方法论,有底气碰数据库。
反过来不行。一上来就做 ChatBI,权限体系没经验、评估集是空的、用户信任也没建立,一次 SQL 翻车就能让整个 AI 项目失去信任。
总结
三个场景做下来,我最大的感受是:给传统 SaaS 加 AI,技术方案都现成,难的是决定做什么、不做什么。
回看三条主线:
从外围到核心。 客服是产品外围,助手在业务流程里,ChatBI 直接碰数据资产。每一步都比上一步更接近业务本质,风险也更大。
从只读到可写。 客服只回答问题,助手生成草稿等人确认,未来 Agent 会直接执行操作。写权限是分阶段放开的,每放开一步都要有配套的确认机制。
从规则到 Agent。 FAQ 匹配是规则,工具调用是半自主,ChatBI 的"生成-执行-自检-重试"已经是 Agent 的基本循环了。
这也正好引出下一篇:Agent 基础原理:从 ReAct 模式到 Plan-and-Execute。ChatBI 那个"生成 SQL、执行、看结果、不行重来"的循环,就是 Agent 的雏形。下一篇把 Agent 的底层原理讲透:它为什么能自主完成任务,又为什么经常翻车。
我们下篇见。
顺便问一句:这三个场景,你们团队目前在做哪一个?还是卡在了权限或 SQL 幻觉上?欢迎在评论区聊聊,我看到都会回。