# 从 RAG 到 Agent:社保智能客服的进化(下)——多模态与完
本文介绍了社保智能客服Agent在多模态交互与完整业务流程中的实现方案。主要内容包括: 两种业务流程设计: 字段流(fields):适用于纯信息收集类业务,如社保转移需收集4个字段 步骤流(steps):适用于需要多模态交互的业务,如养老金认证需身份证OCR和人脸核验 关键技术实现: 身份证OCR采用PaddleOCR解析,通过正则提取关键信息并自动填充表单 人脸核验实现活体检测+1:1比对,与上
从 RAG 到 Agent:社保智能客服的进化(下)——多模态与完整链路
[上篇] 我们搭好了 Agent 的"脑子"——Function Calling 意图识别 + 七态状态机 + Session 管理 + Config 驱动。这篇看 Agent 怎么把"手"和"脚"伸出去——OCR 拍照识别、人脸核验、语音交互,然后跟着一个完整案例从头走到尾。
一、两种业务流程:fields vs steps
上篇提过,Agent 支持两种流程分支:
字段流(fields):逐字段文字收集。适合纯信息类业务——查养老金要身份证号,社保转移要四个字段,参保登记要姓名+身份证+手机号。
步骤流(steps):多步操作,每一步可能是拍照、人脸扫描。适合需要"真东西"的业务——养老金资格认证要传身份证照片、要做人脸活体检测。
# 字段流:社保关系转移
- id: "social_transfer"
type: "handle"
fields:
- key: "from_city"
label: "转出城市"
type: "city_select"
- key: "to_city"
label: "转入城市"
- key: "id_number"
label: "身份证号"
type: "idcard"
- key: "name"
label: "姓名"
type: "text"
require_confirm: true
# 步骤流:养老金资格认证
- id: "pension_auth"
type: "handle"
steps:
- step: "idcard_photo" # 第一步:拍身份证
type: "photo"
label: "身份证正面照片"
prompt: "请上传您的身份证正面照片"
field_maps: # OCR结果自动填入字段
name: "name"
id_number: "id_number"
- step: "face_scan" # 第二步:人脸核验
type: "face_scan"
label: "人脸识别认证"
require_prev_photo: "idcard_photo" # 引用上一步的身份证照片
require_confirm: true
两种流互不冲突,同一个 FlowController 根据 business.steps 是否存在自动选择分支。
二、身份证 OCR:PaddleOCR 解析证件
步骤流第一步通常是身份证拍照。后端收到图片后:
def process_idcard_photo(self, image_bytes: bytes, user_id: str) -> dict:
# 1. 上传 MinIO 留存
object_name = self._storage.upload_bytes(image_bytes, "jpg",
sub_dir=f"idcard/{user_id}/")
# 2. 写入临时文件,调 PaddleOCR
with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as tmp:
tmp.write(image_bytes)
tmp_path = tmp.name
result = self._ocr.ocr(tmp_path, cls=True)
text_lines = self._extract_text(result)
# 3. 正则解析:姓名、身份证号、地址
parsed = self._parse_idcard(text_lines)
return {
"file_url": self._storage.get_url(object_name),
"object_name": object_name,
"name": parsed.get("name", ""),
"id_number": parsed.get("id_number", ""),
"address": parsed.get("address", ""),
"valid": bool(parsed.get("id_number") and parsed.get("name")),
}
解析逻辑不是 OCR 模型自带的,是正则提取:
@staticmethod
def _parse_idcard(text_lines):
result = {"name": "", "id_number": "", "address": ""}
full_text = "\n".join(text_lines)
# 身份证号:18位数字/X
id_match = re.search(r"([1-9]\d{16}[\dXx])", full_text)
if id_match:
result["id_number"] = id_match.group(1)
# 姓名:先匹配"姓名:"标签,找不到则取2-4个连续中文
name_match = re.search(r"姓名\s*[::]\s*(.+)", full_text)
if name_match:
result["name"] = name_match.group(1).strip()
else:
for line in text_lines:
if 2 <= len(line) <= 4 and all("\u4e00" <= c <= "\u9fff" for c in line):
result["name"] = line
break
return result
OCR 结果不会 100% 准确。所以 valid 字段交给调用方判断——姓名和身份证号都识别到了才算通过,否则提示用户重新拍摄。
识别成功后,field_maps 把 OCR 结果自动填入 collected 字段:
field_maps:
name: "name" # OCR的name → 收集字段的name
id_number: "id_number" # OCR的id_number → 收集字段的id_number
用户不用手打姓名和身份证号,拍照一次就够了。
三、人脸核验:活体检测 + 1:1 比对
认证类业务(养老金资格认证)的第二步是人脸核验。配置里用 require_prev_photo 关联上一步的身份证照片:
- step: "face_scan"
type: "face_scan"
label: "人脸识别认证"
prompt: "请将面部对准取景框,按提示完成眨眼/张嘴/摇头动作"
require_prev_photo: "idcard_photo" # 用身份证照片做比对基准
FlowController.handle_face() 拿到用户现场拍摄的人脸图,加上 session 里存的上一步身份证照片的 MinIO 路径:
def handle_face(self, user_id, face_image_bytes):
session = self._store.get(user_id)
current_step = session.get("current_step", {})
# 从 session 获取上一步的身份证照片
prev_step_name = current_step.get("require_prev_photo")
prev_result = session["step_results"].get(prev_step_name, {})
idcard_obj = prev_result.get("object_name") # MinIO 文件路径
# 调人脸服务:活体检测 + 人脸比对
ok, msg = handler.verify(face_image_bytes, idcard_obj)
if not ok:
return {"content": f"{msg}\n\n请重新进行人脸扫描。"}
# 通过后记录结果,推进到确认或完结
step_results[current_step["step"]] = {"status": "passed"}
return self._finalize(user_id, business, session)
当前版本人脸服务的活体检测和比对接口是桩(stub),返回固定成功结果。真实对接在规划中。
核验通过后,face_verified 标志写入 collected,证明"这个人拍了身份证、又通过了人脸比对"——业务接口收到这个字段就知道用户完成了实名认证。
四、完整案例:社保关系转移,从头走到尾
拿 社保关系转移 跑一遍完整链路。
Step 1:用户表达意图
用户: 我要把社保从杭州转到北京
Agent 处于 idle 状态。IntentEngine 把用户输入 + 业务列表送入 LLM。LLM 调用 match_business:
{
"intent": "match_business",
"arguments": {
"business_id": "social_transfer",
"reason": "用户明确表达了跨城市社保转移的意图,提到了转出地杭州和转入地北京"
}
}
FlowController 从 config 读取 social_transfer 的定义——4 个字段、需要确认、类型为 handle。进入 collecting 状态,返回第一条提示:
好的,您要办理的是【社保关系转移】,请提供以下信息:
(1/4) 请输入您原来参保的城市
Step 2:逐字段收集
用户: 杭州
LLM 在 collecting 上下文中,当前字段是 from_city,用户输入"杭州"——provide_field(field_key="from_city", field_value="杭州")。校验通过(中文城市名),存入 collected。推进到下一字段:
(2/4) 请输入您要转入的城市
用户: 北京
→ 校验通过 → 推进。
(3/4) 请输入您的身份证号
用户: 320102199001011234
→ ID card 校验器检查格式(18 位数字/X)+ 校验位计算,通过 → 推进。
(4/4) 请输入您的姓名
用户: 张三
→ 校验通过,全部字段收齐。
Step 3:确认提交
FlowController 检测到 next_idx >= len(fields),调用 _finalize()。social_transfer 配置了 require_confirm: true,进入 confirming 状态:
请确认以上信息无误后,点击确认提交社保关系转移申请。
汇总信息:
- 社保关系转移
- 转出城市: 杭州
- 转入城市: 北京
- 身份证号: 32****34
- 姓名: 张三
回复"确认"提交,回复"取消"退出。
注意身份证号做了脱敏显示(32****34),前端不会暴露完整证件号。但 collected 里存的是原始值,调接口时用。
用户: 确认
LLM 在 confirming 上下文中识别为 confirm_submit。FlowController 进入 executing 状态,调用 ApiCaller:
def _execute_api(self, user_id, business, session):
collected = session["collected"]
step_results = session.get("step_results", {})
# 合并字段收集结果和步骤结果
merged = dict(collected)
for step_name, step_result in step_results.items():
for step_def in business.get("steps", []):
if step_def["step"] == step_name:
for src_key, dest_key in step_def.get("field_maps", {}).items():
merged[dest_key] = step_result.get(src_key, merged.get(dest_key, ""))
# 调业务接口
api_result = self._api.call(business, merged)
return self._api.format_result(business, api_result)
当前
ApiCaller为 Mock 实现,返回预设的 JSON。真实业务接口通过 config 中的${变量}配置,对接后直接生效。
返回结果:
办理成功!您的社保关系转移申请已提交。
受理编号:TRF-2025-05007-001
预计处理时间:15 个工作日
请问还有什么可以帮您的?
状态回到 done。Session 保留 30 分钟,用户随时可以回来继续。
五、输入校验:不止是正则
FieldValidator 用注册模式支持多种字段类型:
@FieldValidator.register("idcard")
def validate_idcard(value, field_def):
if not re.match(r"^\d{17}[\dXx]$", value):
return False, "格式不正确,应为18位"
# 校验位计算
weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
check_codes = "10X98765432"
total = sum(int(value[i]) * weights[i] for i in range(17))
expected = check_codes[total % 11]
if value[17].upper() != expected:
return False, "校验位不正确,请检查"
return True, ""
@FieldValidator.register("phone")
def validate_phone(value, field_def):
if not re.match(r"^1[3-9]\d{9}$", value):
return False, "格式不正确,应为11位手机号"
return True, ""
内置类型包括 idcard(含校验位)、phone、year(2000-当前年份范围)、enum(选项匹配,支持数字和文本)、city_select(正则匹配中文城市名)。新增校验器只需加一个 @FieldValidator.register("xxx") 装饰器。
校验失败时返回的 error_msg 来自 config 中的字段定义,不用硬编码在代码里:
- key: "id_number"
type: "idcard"
validate: "^\\d{17}[\\dXx]$"
error_msg: "身份证号格式不正确,应为18位"
六、多模态:不止打字
上篇和下篇都在讲文本交互,但 Agent 的输入通道远不止于此。前端集成了讯飞语音识别(IAT)和语音合成(TTS),用户可以直接语音提问,回答自动播报。拍照和人脸采集通过浏览器直接调摄像头,Base64 编码后走 /agent/photo/upload 和 /agent/face/verify 两个接口。
这些不是 Agent 的核心创新,但它让 Agent 从"开发者的玩具"变成了"群众能用的东西"——很多办事的人不会打字、不想打字、或者手边没键盘。语音 + 拍照 + 人脸,三个通道覆盖了真实的交互场景。
七、路由设计一览
Flask 服务一共 10 个路由,分三组:
RAG 通道(兼容上一篇):
| 路由 | 方法 | 说明 |
|---|---|---|
/getAnser |
GET | RAG 同步问答 |
/getAnserStream |
GET | RAG 流式问答 |
/AI |
GET | 前端页面 |
Agent 通道:
| 路由 | 方法 | 说明 |
|---|---|---|
/agent/chat |
POST | 文本对话(非流式) |
/agent/chat/stream |
POST | 文本对话(模拟流式) |
/agent/session |
GET | 查询当前会话状态 |
/agent/session |
DELETE | 重置会话 |
/agent/photo/upload |
POST | 上传身份证照片 |
/agent/face/verify |
POST | 人脸核验 |
认证:
| 路由 | 方法 | 说明 |
|---|---|---|
/auth/login |
POST | JWT 登录,返回 token |
Agent 通道的接口都带 @auth.require_auth 装饰器,通过 Bearer token 鉴权。auth.enabled: false 可关闭。
八、诚实交代
这篇博客写到的东西,不是全部都已经对接了生产系统。实话:
-
业务 API(
ApiCaller):当前是 Mock 实现,返回硬编码的 JSON 响应。真实业务接口在对接中,架构层面已预留了config.yaml中的${VARIABLE}环境变量 +field_mapping字段映射,对接时改配置即可。 -
人脸服务(
FaceHandler):活体检测和 1:1 比对接口是桩,返回固定成功。人脸算法本身不是这个项目自研的,需要对接第三方服务。 -
前端:当前是原生 HTML/CSS/JS,语音用了讯飞 SDK,整体可用但不算精致。后续考虑迁移到 Vue/React。
这些不是藏着掖着的缺陷,是分步落地的正常节奏——框架先跑通,接口一个一个接。博客的价值在于把架构讲清楚,读者拿去可以改、可以接自己的东西。
九、总结:RAG 到 Agent,变了什么
回看两篇的完整进化线:
| 维度 | RAG(上一篇) | Agent(上+下篇) |
|---|---|---|
| 交互模式 | 一问一答 | 多轮对话 + 流程引导 |
| 意图识别 | 无,全走检索 | Function Calling,根据状态切换工具集 |
| 状态管理 | 无状态 | 七态状态机 + Redis 持久化 |
| 业务处理 | 只答不问 | 收集字段 → 确认 → 调接口 |
| 输入通道 | 文本 | 文本 + 语音 + 拍照 + 人脸 |
| 扩展方式 | 改代码 | 改 YAML 配置 |
| 主动性 | 被动回答 | 主动推荐业务办理 |
这套 Agent 框架的内核是可复用的。Config 里的业务类型换成政务服务、企业审批、银行开户,骨架不变。如果你也在从 RAG 往 Agent 走,希望这两篇能省你一点摸索的时间。
相关链接
- 上篇:从 RAG 到 Agent(上)——意图识别与状态机
- DeepSeek Function Calling:https://api-docs.deepseek.com/guides/function_calling
- PaddleOCR:https://github.com/PaddlePaddle/PaddleOCR
- 项目源码:https://github.com/xxx/si_agent
更多推荐


所有评论(0)