从 RAG 到 Agent社保智能客服的进化下——多模态与完整链路[上篇] 我们搭好了 Agent 的脑子——Function Calling 意图识别 七态状态机 Session 管理 Config 驱动。这篇看 Agent 怎么把手和脚伸出去——OCR 拍照识别、人脸核验、语音交互然后跟着一个完整案例从头走到尾。一、两种业务流程fields vs steps上篇提过Agent 支持两种流程分支字段流fields逐字段文字收集。适合纯信息类业务——查养老金要身份证号社保转移要四个字段参保登记要姓名身份证手机号。步骤流steps多步操作每一步可能是拍照、人脸扫描。适合需要真东西的业务——养老金资格认证要传身份证照片、要做人脸活体检测。# 字段流社保关系转移-id:social_transfertype:handlefields:-key:from_citylabel:转出城市type:city_select-key:to_citylabel:转入城市-key:id_numberlabel:身份证号type:idcard-key:namelabel:姓名type:textrequire_confirm:true# 步骤流养老金资格认证-id:pension_authtype:handlesteps:-step:idcard_photo# 第一步拍身份证type:photolabel:身份证正面照片prompt:请上传您的身份证正面照片field_maps:# OCR结果自动填入字段name:nameid_number:id_number-step:face_scan# 第二步人脸核验type:face_scanlabel:人脸识别认证require_prev_photo:idcard_photo# 引用上一步的身份证照片require_confirm:true两种流互不冲突同一个FlowController根据business.steps是否存在自动选择分支。二、身份证 OCRPaddleOCR 解析证件步骤流第一步通常是身份证拍照。后端收到图片后defprocess_idcard_photo(self,image_bytes:bytes,user_id:str)-dict:# 1. 上传 MinIO 留存object_nameself._storage.upload_bytes(image_bytes,jpg,sub_dirfidcard/{user_id}/)# 2. 写入临时文件调 PaddleOCRwithtempfile.NamedTemporaryFile(suffix.jpg,deleteFalse)astmp:tmp.write(image_bytes)tmp_pathtmp.name resultself._ocr.ocr(tmp_path,clsTrue)text_linesself._extract_text(result)# 3. 正则解析姓名、身份证号、地址parsedself._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)andparsed.get(name)),}解析逻辑不是 OCR 模型自带的是正则提取staticmethoddef_parse_idcard(text_lines):result{name:,id_number:,address:}full_text\n.join(text_lines)# 身份证号18位数字/Xid_matchre.search(r([1-9]\d{16}[\dXx]),full_text)ifid_match:result[id_number]id_match.group(1)# 姓名先匹配姓名标签找不到则取2-4个连续中文name_matchre.search(r姓名\s*[:]\s*(.),full_text)ifname_match:result[name]name_match.group(1).strip()else:forlineintext_lines:if2len(line)4andall(\u4e00c\u9fffforcinline):result[name]linebreakreturnresultOCR 结果不会 100% 准确。所以valid字段交给调用方判断——姓名和身份证号都识别到了才算通过否则提示用户重新拍摄。识别成功后field_maps把 OCR 结果自动填入collected字段field_maps:name:name# OCR的name → 收集字段的nameid_number:id_number# OCR的id_number → 收集字段的id_number用户不用手打姓名和身份证号拍照一次就够了。三、人脸核验活体检测 1:1 比对认证类业务养老金资格认证的第二步是人脸核验。配置里用require_prev_photo关联上一步的身份证照片-step:face_scantype:face_scanlabel:人脸识别认证prompt:请将面部对准取景框按提示完成眨眼/张嘴/摇头动作require_prev_photo:idcard_photo# 用身份证照片做比对基准FlowController.handle_face()拿到用户现场拍摄的人脸图加上 session 里存的上一步身份证照片的 MinIO 路径defhandle_face(self,user_id,face_image_bytes):sessionself._store.get(user_id)current_stepsession.get(current_step,{})# 从 session 获取上一步的身份证照片prev_step_namecurrent_step.get(require_prev_photo)prev_resultsession[step_results].get(prev_step_name,{})idcard_objprev_result.get(object_name)# MinIO 文件路径# 调人脸服务活体检测 人脸比对ok,msghandler.verify(face_image_bytes,idcard_obj)ifnotok:return{content:f{msg}\n\n请重新进行人脸扫描。}# 通过后记录结果推进到确认或完结step_results[current_step[step]]{status:passed}returnself._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_keyfrom_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 状态调用ApiCallerdef_execute_api(self,user_id,business,session):collectedsession[collected]step_resultssession.get(step_results,{})# 合并字段收集结果和步骤结果mergeddict(collected)forstep_name,step_resultinstep_results.items():forstep_definbusiness.get(steps,[]):ifstep_def[step]step_name:forsrc_key,dest_keyinstep_def.get(field_maps,{}).items():merged[dest_key]step_result.get(src_key,merged.get(dest_key,))# 调业务接口api_resultself._api.call(business,merged)returnself._api.format_result(business,api_result)当前ApiCaller为 Mock 实现返回预设的 JSON。真实业务接口通过 config 中的${变量}配置对接后直接生效。返回结果办理成功您的社保关系转移申请已提交。 受理编号TRF-2025-05007-001 预计处理时间15 个工作日 请问还有什么可以帮您的状态回到 done。Session 保留 30 分钟用户随时可以回来继续。五、输入校验不止是正则FieldValidator用注册模式支持多种字段类型FieldValidator.register(idcard)defvalidate_idcard(value,field_def):ifnotre.match(r^\d{17}[\dXx]$,value):returnFalse,格式不正确应为18位# 校验位计算weights[7,9,10,5,8,4,2,1,6,3,7,9,10,5,8,4,2]check_codes10X98765432totalsum(int(value[i])*weights[i]foriinrange(17))expectedcheck_codes[total%11]ifvalue[17].upper()!expected:returnFalse,校验位不正确请检查returnTrue,FieldValidator.register(phone)defvalidate_phone(value,field_def):ifnotre.match(r^1[3-9]\d{9}$,value):returnFalse,格式不正确应为11位手机号returnTrue,内置类型包括idcard含校验位、phone、year2000-当前年份范围、enum选项匹配支持数字和文本、city_select正则匹配中文城市名。新增校验器只需加一个FieldValidator.register(xxx)装饰器。校验失败时返回的error_msg来自 config 中的字段定义不用硬编码在代码里-key:id_numbertype:idcardvalidate:^\\d{17}[\\dXx]$error_msg:身份证号格式不正确应为18位六、多模态不止打字上篇和下篇都在讲文本交互但 Agent 的输入通道远不止于此。前端集成了讯飞语音识别IAT和语音合成TTS用户可以直接语音提问回答自动播报。拍照和人脸采集通过浏览器直接调摄像头Base64 编码后走/agent/photo/upload和/agent/face/verify两个接口。这些不是 Agent 的核心创新但它让 Agent 从开发者的玩具变成了群众能用的东西——很多办事的人不会打字、不想打字、或者手边没键盘。语音 拍照 人脸三个通道覆盖了真实的交互场景。七、路由设计一览Flask 服务一共 10 个路由分三组RAG 通道兼容上一篇路由方法说明/getAnserGETRAG 同步问答/getAnserStreamGETRAG 流式问答/AIGET前端页面Agent 通道路由方法说明/agent/chatPOST文本对话非流式/agent/chat/streamPOST文本对话模拟流式/agent/sessionGET查询当前会话状态/agent/sessionDELETE重置会话/agent/photo/uploadPOST上传身份证照片/agent/face/verifyPOST人脸核验认证路由方法说明/auth/loginPOSTJWT 登录返回 tokenAgent 通道的接口都带auth.require_auth装饰器通过 Bearer token 鉴权。auth.enabled: false可关闭。八、诚实交代这篇博客写到的东西不是全部都已经对接了生产系统。实话业务 APIApiCaller当前是 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 Callinghttps://api-docs.deepseek.com/guides/function_callingPaddleOCRhttps://github.com/PaddlePaddle/PaddleOCR项目源码https://github.com/xxx/si_agent