20年老程序员×AI:2小时搭建社保智能客服系统实战
20年老程序员×AI:2小时搭建社保智能客服系统实战
一、背景
去年用 Python 现学现卖做了一个社保知识 RAG 问答系统——用 Milvus 向量库 + Ollama(BGE-M3) + DeepSeek,用户问政策,系统从知识库里找最像的问题喂给大模型回答。
跑了一段时间发现不对——用户不只是想"问问题",他们想办事:查养老金发了多少、转移社保关系、开参保证明、异地就医备案……这就需要从"单轮 QA"变成"有状态的多轮 Agent"。
下面是我和 AI 一起从头搭建这个 Agent 系统的完整过程。先说结论:一个人,2-3 小时,12 个业务类型,后端+前端+语音+HTTPS 全栈。传统开发哪个 10 人团队不得干两周?
二、核心对话实录
回合1:桩/真分层——开发效率的第一刀
我:后端接口现在先全部用桩模块实现……LLM、Milvus、OCR 真实,人脸服务、业务 API 桩模拟。
这个需求背后的经验判断:如果全真,卡在联调上一个服务不通整体跑不起来;如果全桩,验证不了核心链路。真/假分层后,LLM 的 Function Calling、向量检索、OCR 身份证识别走真实服务,验证核心能力;人脸和业务 API 走桩返回模拟数据,保证流程能从头跑到尾。
AI 改了 2 个文件:face_handler.py 去掉 requests 调外部接口,固定返回通过;api_caller.py 根据 business_id 返回对应的模拟 JSON。
回合2:字段校验失败跳 QA
我: 43103 输入后没有继续要身份证号,跳去回答问题了。
这是一个典型的边界条件 AI 自己想不到。用户办异地就医备案,我输入 43103(不完整的身份证号),系统回答"您好,社会保障号码就是您的身份证号码……"——把残缺输入当成了提问。
原因是 _handle_collecting 里的逻辑:LLM 把 43103 识别为 answer_question,校验失败后优先走了 QA 路径。修法很简单:校验失败一律返回纠错提示,不跳 QA。
# 去掉这个分支:
# if intent == "answer_question":
# return self._handle_qa(...)
# 验证失败始终:return {"type": "text", "content": validated_value, "next_action": "collecting"}
回合3:"办理"没有上下文
我: 系统推荐了"💡 您可能需要办理【异地就医备案】,回复’办理’即可开始",用户回"办理",系统却又让重新说办什么。
用户说"办理"两个字,LLM 不知道办什么——它没看见上一轮的推荐。修法:给 IntentEngine 的 LLM 调用注入最少最近 6 条对话历史作为上下文。
def _build_messages(self, system_content, user_text, session):
messages = [{"role": "system", "content": system_content}]
history = session.get("history", [])
if history:
for h in history[-6:]:
messages.append({"role": h.get("role"), "content": h.get("content", "")[:300]})
messages.append({"role": "user", "content": user_text})
return messages
加系统指令:“如果用户说办理、好的、可以等简短确认词,参考对话历史中最近一次推荐的业务来判定。”
效果:有历史→match_business,无历史→answer_question。
回合4:JSON 转卡片
我:前端用户直接看到 JSON,太不友好了。
业务 API(桩)返回的 JSON 被原样展示为代码块。加了一个 renderJsonBlocks() 函数,解析 <pre><code class="language-json"> 块,转成 key-value 表格卡片。配了 40+ 英译中字段映射表:
var labelMap = {
'id_card': '身份证号', 'monthly_pension': '月养老金',
'application_id': '申请编号', 'direct_settlement': '直接结算', ...
};
嵌套 object 不再显示 [object Object],转成 JSON 字符串展示。
回合5:聊天历史丢了
我:前端聊天记录历史丢了。
原来是每条新消息直接覆盖 #result div。改成追加模式——用户消息蓝色气泡右对齐,AI 回复白色气泡左对齐 + 头像,chat-history 容器自动滚底。
回合6-9:语音按钮——4 次迭代踩坑
这是与讯飞 IAT SDK 集成时踩的坑,也是整个开发过程里调最久的部分。
第一版需求:按住不放开始录音,松开提交文本,10 秒无语音自动放弃。
坑1:点了没反应。原因:btn.onclick = null 之后才去读 origClick,永远是 null → 先把原始点击保存了再清空。
坑2:一直"连接中…“,不到"录音中…”。原因:window.iatWS 不存在——index.js 里是 let iatWS,不在 window 上 → 改 typeof iatWS !== 'undefined' 直接引用。
坑3:松开后文字为空。原因:松开→立即关 WebSocket,截断了正在回来的识别结果 → 改成松开只停录音,等 WebSocket 收到 status=2 自然关闭后延迟 300ms 再取文字。
坑4:同事反馈——“要考虑建立通道的时间,不能让客户白说” → 分两段:按住→"连接中…"→WebSocket 就绪→"录音中…"→现在可以说话了。
// 最终流程
btn.innerText = '连接中...'; // 按下的瞬间
// checkReady 轮询 iatWS.readyState === 1
btn.innerText = '录音中...'; // 就绪后
// 说话...
// 松开 → '处理中...' → 等 close → 取文字 → 提交
坑5:语音识别结果自带句号。讯飞识别结果 “北京市。” 被 ^\d{17}[\dXx]$ 校验拦下 → 加 rstrip('\u3002...') 去掉中文标点。顺便加了中文日期智能解析:“026年3月5号” → 2026-03-05。
回合10:并发安全
我:要支持并发,多个客户的上下文不能混在一起。
Redis key 天然按 agent:session:{user_id} 隔离。FlowController 单例加双重检查锁:
_flow_lock = threading.Lock()
def _get_flow():
global flow
if flow is None:
with _flow_lock:
if flow is None:
flow = FlowController(...)
return flow
SessionStore 加了 Redis 不可用时的内存回退,不依赖特定环境。
回合11:RAG 老说"根据上下文"
我:回答问题里面出现了根据上下文。
Prompt 调了一轮:从"不要提示根据提供的上下文"改成"严禁说根据上下文、参考以上信息、根据提供的内容之类的话,要像自己本来就知道一样直接回答"。顺手修了一个 bug——for 循环里的 q 变量名覆盖了用户原始问题。
回合12:可配置化——业务的灵魂
我:业务太少,还要有参保登记、个人信息修改、异地就医备案等——可配置化。
核心设计:业务规则不进代码,只改 YAML。字段定义、校验规则、API 映射全部配置化:
- id: "remote_medical"
name: "异地就医备案"
type: "handle"
fields:
- key: "medical_city"
label: "就医城市"
validate: "^[\\u4e00-\\u9fa5]{2,10}(?:市|区|县)?$"
- key: "hospital"
label: "就医医院"
...
require_confirm: true
config.yaml 里新增一个业务,api_caller.py 加一段桩数据,前端 bizNameMap 补一行——不写任何业务逻辑代码。从 5 个业务扩到 12 个,纯加配置。
最终 12 个业务:养老金发放查询、缴费记录查询、养老金资格认证、社保关系转移、参保证明开具、参保登记、个人信息修改、异地就医备案、社保卡申领、医疗费用报销、失业保险金申领、生育保险申领。
回合13:身份证后置
我:一个销售同事提的——身份证号是敏感信息,应该最后再要求客户输入,容易获取信赖。我考虑后觉得合理。
12 个业务类型,凡是含 id_number 字段的,全部调到最后一个。用户先回答城市、医院、日期这些低敏感信息,建立信任感之后,最后才要身份证号。config.yaml 里重新排序即可,代码不动。
回合14:手机端语音需要 HTTPS
我:要有 HTTPS,要不手机端语音功能用不了——
getUserMedia在 HTTP 下被禁止。
OpenSSL 生成自签名证书,启动代码加双模式:
# 默认 HTTPS 443(手机端可用语音)
# $env:FLASK_HTTP=1 → HTTP 5000(本地调试)
if os.environ.get("FLASK_HTTP") == "1":
app.run(host='0.0.0.0', port=5000, debug=True)
else:
app.run(host='0.0.0.0', port=443, ssl_context=(cert_path, key_path), debug=True)
三、关于 AI 写代码这件事
知道好的代码长什么样,让 AI 写。 判断力比编码力更稀缺。
创造从"想要什么"开始,不是从"会什么"开始。 AI 把"去找什么"的时间从 3 天压到 3 分钟而已。
四、技术栈
| 层 | 技术 |
|---|---|
| LLM | DeepSeek API (deepseek-chat) |
| 向量化 | Ollama (bge-m3) |
| 向量数据库 | Milvus |
| 会话存储 | Redis (30min TTL, 内存回退) |
| 意图识别 | Function Calling + 对话历史上下文 + 业务推荐 |
| 前端 | Flask + jQuery + AmazeUI + 讯飞 IAT/TTS + marked.js |
| HTTPS | Flask SSL + OpenSSL 自签名证书 |
人脸服务和业务 API 当前用桩实现,联调时替换即可。
五、结语
有人说 AI coding 提升效率 50%,有人说 10%。看看这个项目的实际数据:传统开发 11-17 天,实际 2-3 小时——效率提升至少 80%。原因有两个:
- 项目类型恰好是 AI 最强项:样板代码、配置驱动、CRUD、前端适配
- 经验差决定了 AI 的放大倍数:初级程序员不知道该做什么、不该做什么、哪里会出问题。经验让 AI 从"能跑的代码生成器"变成"精准的工具"
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)