从 RAG 到 Agent:社保智能客服的进化(上)——意图识别与状态机


一、从上一篇说起

上一篇 我们搭了一套基于 Milvus + Ollama + DeepSeek 的 RAG 智能客服:用户问什么,系统从知识库检索相关内容,喂给大模型生成回答。一条直线:问题 → 向量化 → 检索 → 生成 → 回答

部署之后开始真的有人用了,问题来了:很多人不是来问政策的,是来办事的。

“我要查养老金发了多少”——这不只是问答,是查询类业务,需要收集身份证号、调接口。

“我要把社保从杭州转到北京”——要收三个字段、确认、提交。

“帮我做个养老金资格认证”——要拍照、要人脸识别,光打字不够。

RAG 管得了"说",管不了"做"。所以我们要从 RAG 升级到 Agent

这不是换模型,是加一层脑子。


二、Agent 比 RAG 多了什么

如果把 RAG 比作一个图书管理员——你问什么,他翻书告诉你——那 Agent 是一个柜台办事员

  1. 判断你要干什么(问答、查询、还是办事)

  2. 一步步向你要材料(字段收集)

  3. 调用外部工具(拍照、人脸识别、调业务接口)

  4. 记住你跟他说过什么(多轮对话状态)

  5. 确认后提交,告诉你结果

技术对应:

| RAG | Agent |

|-----|-------|

| 无状态,一问一答 | 多轮会话状态机 |

| 单一 prompt | 按状态切换 Function Calling 工具集 |

| 只读:检索知识库 | 读写:收集信息 → 调 API → 返回结果 |

| 文本输入输出 | 文本 + 拍照 + 人脸 + 语音 |


三、整体架构


用户(文本/语音/拍照/人脸)

         │

         ▼

    ┌─────────────┐

    │   Flask 服务   │  ← JWT 鉴权 + HTTPS

    └──────┬──────┘

           │

    ┌──────┴──────────────────────────┐

    │          Agent Core              │

    │                                  │

    │  ┌────────────┐  ┌────────────┐  │

    │  │ IntentEngine│  │FlowController│ │

    │  │ (意图识别)   │─▶│ (状态机编排)  │ │

    │  └─────┬──────┘  └──────┬─────┘  │

    │        │                │         │

    │   DeepSeek          ┌───┴───┐     │

    │   Function          │       │     │

    │   Calling      SessionStore  │     │

    │                (Redis/内存)   │     │

    │                             │     │

    │  ┌──────────┐ ┌──────────┐  │     │

    │  │PhotoHandler│ │ApiCaller │  │     │

    │  │(身份证OCR) │ │(业务接口) │  │     │

    │  └──────────┘ └──────────┘  │     │

    │  ┌──────────┐               │     │

    │  │FaceHandler│              │     │

    │  │(人脸核验) │              │     │

    │  └──────────┘               │     │

    └─────────────────────────────┘

           │

    ┌──────┴──────┐

    │   RAG 通道   │  ← 退路:知识问答

    │ (Milvus+LLM) │

    └─────────────┘

Agent 不是替代 RAG,是 RAG 的上层建筑。用户来了先走 Agent 流程;Agent 判断这只是一个知识问题,退回 RAG 通道处理。


四、Function Calling:三种上下文,三套工具

Agent 的"脑子"是 IntentEngine。它基于 OpenAI 兼容的 Function Calling,根据用户当前所处的状态,给 LLM 提供不同的工具集

4.1 空闲状态(idle)——判断你要干什么

系统刚启动时,用户处于 idle 状态。LLM 有两把工具:


tools = [

    {

        "type": "function",

        "function": {

            "name": "match_business",

            "description": "识别用户想要办理或查询的社保业务类型",

            "parameters": {

                "properties": {

                    "business_id": {

                        "enum": ["pension_query", "insurance_record", 

                                 "social_transfer", "card_apply", ...]

                    }

                }

            }

        }

    },

    {

        "type": "function",

        "function": {

            "name": "answer_question",

            "description": "用户在进行社保知识问答咨询",

            "parameters": {

                "properties": {

                    "suggested_business_id": {

                        "description": "如果用户咨询暗示可能想办理某项业务,填写对应ID"

                    }

                }

            }

        }

    }

]

关键设计:answer_question 里藏了一个 suggested_business_id。用户问"社保转移要什么材料",本质上是想办转移。LLM 回答完知识后,系统会自动附一句:“您可能需要办理社保关系转移,回复’办理’即可开始”。

这就是 Agent 比 RAG 多出来的主动性

4.2 收集状态(collecting)——一步步要材料

进入业务办理流程后,LLM 的工具变成三把:


tools = [

    {"name": "provide_field"},   # 用户提供了字段值

    {"name": "cancel_flow"},     # 用户想取消

    {"name": "answer_question"}, # 用户问了无关问题

]

provide_field 不穷举字段。LLM 从当前 system prompt 里知道正在收集哪个字段,自行从用户输入里提取值。用户说了"320102199001011234",LLM 判断这是身份证号,自动填入。

对话历史也在上下文里。用户上一轮说了"我要查缴费记录",这一轮说"2020年",LLM 能结合历史推断这是开始年份。

4.3 确认状态(confirming)——确认 / 取消 / 修改

信息收齐后进入确认状态:


tools = [

    {"name": "confirm_submit"},

    {"name": "cancel_flow"},

    {"name": "modify_info", "parameters": {"field_key": "要修改的字段"}},

]

用户说"手机号写错了" → LLM 调用 modify_info → 系统跳回 collecting 状态让用户重填。用户说"确认" → confirm_submit → 调接口。


五、状态机:七种状态,一条流转线

FlowController 是一个有限状态机。业务类型定义了字段或步骤,控制器按顺序推进:


                    ┌──────────────────┐

                    │       idle       │  ← 等待用户表达意图

                    └────────┬─────────┘

                             │ match_business

                             ▼

                    ┌──────────────────┐

                    │    collecting    │  ← 逐字段收集信息

                    └────────┬─────────┘

                             │ 字段收齐

                    ┌────────┴─────────┐

                    ▼                  ▼

           ┌──────────────┐   ┌──────────────────┐

           │   confirming  │   │  step_executing   │  ← 多步流程(拍照/人脸)

           └──────┬───────┘   └────────┬─────────┘

                  │ confirm_submit     │ 步骤完成

                  ▼                    ▼

           ┌──────────────────────────────────┐

           │            executing              │  ← 调用业务API

           └────────────────┬─────────────────┘

                            │

                            ▼

                    ┌──────────────────┐

                    │       done       │  ← 返回结果

                    └──────────────────┘

两种流程分支:

  • 字段流(fields):大部分业务走这条路。养老金查询收身份证号,社保转移收四个字段,一个个来。

  • 步骤流(steps):需要拍照或人脸的业务(如养老金资格认证),先走身份证 OCR,再走人脸核验,最后确认提交。

状态机的好处是任何时候用户说"取消",都能从任意状态跳回 idle。不会出现"卡在流程里退不出来"的情况。


六、Session 管理:记住每一轮说过什么

RAG 是无状态的——每个请求独立,不记得上一句说了什么。Agent 必须记住上下文。

SessionStore 用 Redis 存储每个用户的会话(Redis 不可用时自动降级为内存存储),TTL 30 分钟。每个 session 包含:


{

    "user_id": "user_001",

    "state": "collecting",         # 当前状态

    "business_id": "social_transfer", # 当前业务

    "current_field_index": 2,      # 收到第几个字段了

    "collected": {                 # 已收集的字段值

        "from_city": "杭州",

        "to_city": "北京"

    },

    "history": [                   # 对话历史(取最近6轮送LLM)

        {"role": "user", "content": "我要转社保"},

        {"role": "assistant", "content": "好的,请提供转出城市..."},

        {"role": "user", "content": "杭州"},

        {"role": "assistant", "content": "请提供转入城市..."},

    ],

    "step_results": {},            # 拍照/人脸的结果

}

对话历史只截取最近 6 轮送入 LLM prompt,控制 token 消耗的同时保持足够的上下文连贯性。


七、Config 驱动:加业务不加代码

这是整个设计里最值钱的决定。所有 12 种业务类型、字段定义、校验规则、API 映射,全在 config.yaml 里:


business_types:

  - id: "social_transfer"

    name: "社保关系转移"

    type: "handle"

    keywords: ["社保转移","跨省转移","换工作","关系转移"]

    fields:

      - key: "from_city"

        label: "转出城市"

        type: "city_select"

        prompt: "请输入您原来参保的城市"

        validate: "^[u4e00-u9fa5]{2,10}(?:市|区|县)?$"

        error_msg: "请输入正确的城市名称"

      - key: "to_city"

        label: "转入城市"

        type: "city_select"

        prompt: "请输入您要转入的城市"

      - key: "id_number"

        label: "身份证号"

        type: "idcard"

        prompt: "请输入您的身份证号"

      - key: "name"

        label: "姓名"

        type: "text"

        prompt: "请输入您的姓名"

    api:

      url: "${SOCIAL_TRANSFER_URL}"

      method: "POST"

      timeout: 30

    require_confirm: true

    confirm_text: "请确认以上信息无误后,点击确认提交社保关系转移申请。"

要新增一种业务?加一段 YAML。不动一行 Python 代码。字段类型(idcardphoneyearenumcity_select)是内置校验器,idcard 甚至做了校验位计算。keywords 辅助 LLM 做意图匹配,prompt 是发给用户的话术,require_confirm 控制是否需要二次确认。

这个设计让 Agent 的业务方可以自己配,不需要每次加业务都找开发改代码。落地场景里,这点比技术本身重要。


八、业务推荐:从问答到办事的无缝衔接

再回头说 suggested_business_id 这个设计。

用户在 idle 状态问了一个知识类问题——比如"社保转移需要什么材料"——LLM 调用 answer_question,同时判断这个问题暗示了 social_transfer 业务。系统正常走 RAG 生成回答,但在回答末尾自动追加:

💡 您可能需要办理【社保关系转移】,回复"办理"即可开始。

用户回一句"办理",LLM 看到对话历史里有上一次推荐的业务,直接匹配 match_business。从"咨询"到"办事",用户不需要重新描述一遍需求。

这个看起来小的功能,在体验上是质变——从被动答疑变成了主动引导


九、关键代码:IntentEngine 的一轮调用


def _call_llm(self, messages, tools):

    payload = {

        "model": "deepseek-chat",

        "messages": messages,

        "tools": tools,

        "tool_choice": "auto",

        "temperature": 0.3,

        "max_tokens": 4096

    }

    resp = requests.post(self._url, json=payload, headers=self._headers, timeout=60)

    result = resp.json()

    

    choice = result["choices"][0]

    msg = choice.get("message", {})

    

    if msg.get("tool_calls"):

        tool_call = msg["tool_calls"][0]

        func_name = tool_call["function"]["name"]

        func_args = json.loads(tool_call["function"]["arguments"])

        return {

            "intent": func_name,

            "arguments": func_args,

            "raw_message": msg.get("content", "")

        }

    

    # LLM 没调工具,当普通问答处理

    return {

        "intent": "answer_question",

        "arguments": {"question": msg.get("content", "")},

    }

DeepSeek 的 Function Calling 完全兼容 OpenAI 格式。tool_choice: "auto" 让模型自己决定调不调工具、调哪个。如果 LLM 正常回了一句文本而不是 tool_call,说明它认为不需要工具——退回 RAG 通道处理。


十、小结

上篇讲的是 Agent 的"脑子":

  1. Function Calling 按状态分三套工具——idle 判断意图,collecting 收集字段,confirming 确认提交

  2. 七态状态机——从 idle 到 done,支持字段流和步骤流两种分支,任何时候可取消

  3. Session 持久化——Redis 存状态,对话历史送 LLM,30 分钟 TTL

  4. Config 驱动——加业务只加 YAML,不动代码

  5. 业务推荐——问答可无缝转化为办事

下篇我们看 Agent 怎么"伸手够真实世界"——身份证 OCR、人脸核验,以及一个完整案例从头走到尾。


相关链接

  • DeepSeek API 文档:https://platform.deepseek.com/api-docs
  • 项目源码:https://github.com/xxx/si_agent
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐