跳转至

withdrawal_customer_service · 智能客服(离退休提取, 完整状态机)

本页收录可运行示例,配套本文档「指南 01」(指南 01)。 代码为随框架交付的示例,已一并收录到本站供直接对照使用。

说明与运行

示例: 智能客服·离退休提取流程(完整案例, 含网关). 业务流程(多轮状态机, 由前端在 variables 中携带 currentStage/flag/exitBusiness 驱动): 1. 首个节点为问题分类(QuestionClassify + 真实 DeepSeek): 用户提问离退休提取相关 → 进入离退休提取流程(nextStage=start) 否则 → 给予公积金知识回答(真实 DeepSeek) 2. 离退休提取流程(前端逐阶段传递 currentStage): (1) currentStage=start → 返回 nextStage=confirmSocialSecurityCard (2) currentStage=confirmSocialSecurityCard、flag=true/false → HttpRequest POST http://localhost:9080/test1(application/json, 携带 flag) 响应 {"code":"xxx","data":{"respMsg":"xxx"}}, 成功 code=FBASE0000 成功 → nextStage=withdrawalCard 失败 → 返回第三方报错文案 data.respMsg + errExit=True 前端收到 errExit=True 后携带 exitBusiness=true 走退出流程 (3) currentStage=withdrawalCard → 清空全部变量为空字符串, nextStage=end (4) exitBusiness=true → 清空全部变量为空字符串, nextStage=end (5) currentStage=userInput → UserInput 暂停让用户输入 恢复后按输入文本更新变量 inputStr, 回传前端 LLM 说明: 不 mock, 真实调用第三方 DeepSeek API(OpenAI 兼容 chat/completions, 经 httpx 直连, 不新增框架依赖); 需设置环境变量 DEEPSEEK_API_KEY。 运行(两个终端):

终端 A: 启动模拟第三方服务(可选, 用于演示 HTTP 成功/失败分支)

PYTHONPATH=src python examples/withdrawal_customer_service.py --mock-third-party

终端 B: 启动网关

PYTHONPATH=src python examples/withdrawal_customer_service.py 请求示例(curl, 阶段驱动):

1) 初始提问 → 分类 → 离退休提取, 返回 nextStage=start

curl -N http://localhost:8080/api/v2/chat/completions -H "Content-Type: application/json" -d '{ "appId": "app-withdraw", "stream": true, "detail": true, "messages": [{"role":"user","content":"我要办理公积金离退休提取"}], "variables": {}}'

2) currentStage=start → nextStage=confirmSocialSecurityCard

curl -N http://localhost:8080/api/v2/chat/completions -H "Content-Type: application/json" -d '{ "appId": "app-withdraw", "stream": true, "messages": [{"role":"user","content":"继续"}], "variables": {"currentStage":"start"}}'

3) 确认社保卡(flag=true) → 第三方成功 → nextStage=withdrawalCard

curl -N http://localhost:8080/api/v2/chat/completions -H "Content-Type: application/json" -d '{ "appId": "app-withdraw", "stream": true, "messages": [{"role":"user","content":"确认"}], "variables": {"currentStage":"confirmSocialSecurityCard","flag":true}}'

4) 确认社保卡(flag=false) → 第三方失败 → data.respMsg + errExit=true

curl -N http://localhost:8080/api/v2/chat/completions -H "Content-Type: application/json" -d '{ "appId": "app-withdraw", "stream": true, "messages": [{"role":"user","content":"确认"}], "variables": {"currentStage":"confirmSocialSecurityCard","flag":false}}'

5) 前端收到 errExit=true → 退出流程(exitBusiness=true) → 清空变量, nextStage=end

curl -N http://localhost:8080/api/v2/chat/completions -H "Content-Type: application/json" -d '{ "appId": "app-withdraw", "stream": true, "messages": [{"role":"user","content":"退出"}], "variables": {"exitBusiness":true}}'

6) 提取完成(currentStage=withdrawalCard) → 清空变量, nextStage=end

curl -N http://localhost:8080/api/v2/chat/completions -H "Content-Type: application/json" -d '{ "appId": "app-withdraw", "stream": true, "messages": [{"role":"user","content":"完成"}], "variables": {"currentStage":"withdrawalCard"}}'

运行前需在已安装 agentframeworkPython ≥ 3.11 环境;示例多为自带本地模型,无需真实 API Key;需要外部依赖(如 chromadb / 数据库驱动)会在说明中注明。

完整源码

"""示例: 智能客服·离退休提取流程(完整案例, 含网关).

业务流程(多轮状态机, 由前端在 variables 中携带 currentStage/flag/exitBusiness 驱动):
    1. 首个节点为问题分类(QuestionClassify + 真实 DeepSeek):
       用户提问离退休提取相关 → 进入离退休提取流程(nextStage=start)
       否则 → 给予公积金知识回答(真实 DeepSeek)
    2. 离退休提取流程(前端逐阶段传递 currentStage):
       (1) currentStage=start             → 返回 nextStage=confirmSocialSecurityCard
       (2) currentStage=confirmSocialSecurityCard、flag=true/false
           → HttpRequest POST http://localhost:9080/test1(application/json, 携带 flag)
             响应 {"code":"xxx","data":{"respMsg":"xxx"}}, 成功 code=FBASE0000
             成功 → nextStage=withdrawalCard
             失败 → 返回第三方报错文案 data.respMsg + errExit=True
             前端收到 errExit=True 后携带 exitBusiness=true 走退出流程
       (3) currentStage=withdrawalCard    → 清空全部变量为空字符串, nextStage=end
       (4) exitBusiness=true              → 清空全部变量为空字符串, nextStage=end
       (5) currentStage=userInput         → UserInput 暂停让用户输入
           恢复后按输入文本更新变量 inputStr, 回传前端

LLM 说明: 不 mock, 真实调用第三方 DeepSeek API(OpenAI 兼容 chat/completions,
经 httpx 直连, 不新增框架依赖); 需设置环境变量 DEEPSEEK_API_KEY。

运行(两个终端):
    # 终端 A: 启动模拟第三方服务(可选, 用于演示 HTTP 成功/失败分支)
    PYTHONPATH=src python examples/withdrawal_customer_service.py --mock-third-party
    # 终端 B: 启动网关
    PYTHONPATH=src python examples/withdrawal_customer_service.py

请求示例(curl, 阶段驱动):
    # 1) 初始提问 → 分类 → 离退休提取, 返回 nextStage=start
    curl -N http://localhost:8080/api/v2/chat/completions -H "Content-Type: application/json" -d '{
      "appId": "app-withdraw", "stream": true, "detail": true,
      "messages": [{"role":"user","content":"我要办理公积金离退休提取"}],
      "variables": {}}'
    # 2) currentStage=start → nextStage=confirmSocialSecurityCard
    curl -N http://localhost:8080/api/v2/chat/completions -H "Content-Type: application/json" -d '{
      "appId": "app-withdraw", "stream": true,
      "messages": [{"role":"user","content":"继续"}],
      "variables": {"currentStage":"start"}}'
    # 3) 确认社保卡(flag=true) → 第三方成功 → nextStage=withdrawalCard
    curl -N http://localhost:8080/api/v2/chat/completions -H "Content-Type: application/json" -d '{
      "appId": "app-withdraw", "stream": true,
      "messages": [{"role":"user","content":"确认"}],
      "variables": {"currentStage":"confirmSocialSecurityCard","flag":true}}'
    # 4) 确认社保卡(flag=false) → 第三方失败 → data.respMsg + errExit=true
    curl -N http://localhost:8080/api/v2/chat/completions -H "Content-Type: application/json" -d '{
      "appId": "app-withdraw", "stream": true,
      "messages": [{"role":"user","content":"确认"}],
      "variables": {"currentStage":"confirmSocialSecurityCard","flag":false}}'
    # 5) 前端收到 errExit=true → 退出流程(exitBusiness=true) → 清空变量, nextStage=end
    curl -N http://localhost:8080/api/v2/chat/completions -H "Content-Type: application/json" -d '{
      "appId": "app-withdraw", "stream": true,
      "messages": [{"role":"user","content":"退出"}],
      "variables": {"exitBusiness":true}}'
    # 6) 提取完成(currentStage=withdrawalCard) → 清空变量, nextStage=end
    curl -N http://localhost:8080/api/v2/chat/completions -H "Content-Type: application/json" -d '{
      "appId": "app-withdraw", "stream": true,
      "messages": [{"role":"user","content":"完成"}],
      "variables": {"currentStage":"withdrawalCard"}}'
"""

from __future__ import annotations

import json
import os
from typing import Any

import httpx
from langchain_core.messages import AIMessage, BaseMessage, HumanMessage

from agentframework import Workflow, serve
from agentframework.nodes import (
    Answer,
    ChatLLM,
    HttpRequest,
    IfElse,
    QuestionClassify,
    UserInput,
    VariableOutput,
    VariableUpdate,
)
from agentframework.runtime.ctx import RunContext

# 第三方接口: 社保卡校验(模拟服务见 run_mock_third_party)
THIRD_PARTY_URL = "http://localhost:9080/test1"
THIRD_PARTY_SUCCESS_CODE = "FBASE0000"


# ---------------------------------------------------------------------------
# 真实 DeepSeek 客户端(OpenAI 兼容 chat/completions, 经 httpx 直连)
# ---------------------------------------------------------------------------
class DeepSeekClient:
    """真实 DeepSeek API 客户端(不 mock).

    实现 LLMClient 期望的协议形态: 具备 invoke(非流式) 与 stream(流式) 方法,
    接收 LangChain BaseMessage 列表, 输出/产出 AIMessage. 经 httpx 直连
    DeepSeek 的 OpenAI 兼容接口, 不新增框架依赖.
    """

    def __init__(
        self,
        api_key: str,
        model: str = "deepseek-chat",
        base_url: str = "https://api.deepseek.com",
        timeout: float = 60.0,
    ) -> None:
        """初始化 DeepSeek 客户端.

        Args:
            api_key: DeepSeek API 密钥(从 DEEPSEEK_API_KEY 环境变量读取).
            model: 模型名(默认 deepseek-chat).
            base_url: API 基础地址(默认 https://api.deepseek.com).
            timeout: 请求超时秒数.
        """
        self.api_key = api_key
        self.model = model
        self.base_url = base_url.rstrip("/")
        self.timeout = timeout

    def _headers(self) -> dict[str, str]:
        return {
            "Authorization": f"Bearer {self.api_key}",
            "Content-Type": "application/json",
        }

    def _openai_messages(self, messages: list[BaseMessage]) -> list[dict[str, str]]:
        """把 LangChain 消息列表转换为 OpenAI 风格消息(role/content)."""
        out: list[dict[str, str]] = []
        for msg in messages:
            mtype = getattr(msg, "type", "user")
            role = "system" if mtype == "system" else "assistant" if mtype == "ai" else "user"
            out.append({"role": role, "content": str(getattr(msg, "content", ""))})
        return out

    def invoke(self, messages: list[BaseMessage]) -> AIMessage:
        """非流式生成: 一次调用返回完整回复(AIMessage)."""
        body = {
            "model": self.model,
            "messages": self._openai_messages(messages),
            "stream": False,
        }
        resp = httpx.post(
            f"{self.base_url}/chat/completions",
            headers=self._headers(),
            json=body,
            timeout=self.timeout,
        )
        resp.raise_for_status()
        content = resp.json()["choices"][0]["message"]["content"]
        return AIMessage(content=content)

    def stream(self, messages: list[BaseMessage]):
        """流式生成: 逐块产出 AIMessage(delta.content), 供 LLMClient 逐块回调."""
        body = {
            "model": self.model,
            "messages": self._openai_messages(messages),
            "stream": True,
        }
        with httpx.stream(
            "POST",
            f"{self.base_url}/chat/completions",
            headers=self._headers(),
            json=body,
            timeout=self.timeout,
        ) as resp:
            resp.raise_for_status()
            for line in resp.iter_lines():
                if not line or not line.startswith("data:"):
                    continue
                payload = line[len("data:") :].strip()
                if payload == "[DONE]":
                    break
                delta = json.loads(payload)["choices"][0]["delta"].get("content")
                if delta:
                    yield AIMessage(content=delta)


# ---------------------------------------------------------------------------
# 流程逻辑(计算函数 / 入口路由 / HTTP 成败判定)
# ---------------------------------------------------------------------------
def _last_user_text(inputs: dict[str, Any], ctx: RunContext) -> str:
    """取对话历史最后一条用户消息文本(供问题分类 query 使用)."""
    last_user = next((m for m in reversed(ctx.messages) if isinstance(m, HumanMessage)), None)
    return str(last_user.content) if last_user else ""


def _flag_value(inputs: dict[str, Any], ctx: RunContext) -> Any:
    """读变量 flag 的值(供 HttpRequest 请求体携带)."""
    return ctx.variables.get("flag")


def _resp_msg(inputs: dict[str, Any], ctx: RunContext) -> Any:
    """从第三方响应取 data.respMsg(供失败文案输出)."""
    data = inputs.get("left")
    if isinstance(data, dict):
        data = data.get("data") or {}
        if isinstance(data, dict):
            return data.get("respMsg")
    return None


def _route_flow(inputs: dict[str, Any], ctx: RunContext) -> str:
    """联合路由: 先分类, 再按 退出/阶段变量 分发.

    优先级: 问题分类结果 > 退出(exitBusiness=true) > 阶段(currentStage).
    只有问题分类为「离退休提取」才进入 退出/阶段 分支, 否则一律 → kb.
    """
    variables = ctx.variables
    # ① 问题分类优先级最高: 非「离退休提取」→ 一律公积金知识回答
    if inputs.get("left") != "离退休提取":
        return "kb"
    # ② 离退休提取内: 退出 > 阶段
    if str(variables.get("exitBusiness", "")).lower() == "true":
        return "exit"
    stage = variables.get("currentStage")
    if stage == "start":
        return "start"
    if stage == "confirmSocialSecurityCard":
        return "confirm"
    if stage == "withdrawalCard":
        return "withdraw"
    if stage == "userInput":
        return "input"
    # ③ 离退休提取且无阶段参数 → 初始化流程: nextStage=start
    return "init"


def _http_ok(inputs: dict[str, Any], ctx: RunContext) -> str:
    """第三方响应判定: code == FBASE0000 为成功, 否则失败."""
    data = inputs.get("left")
    if isinstance(data, dict) and data.get("code") == THIRD_PARTY_SUCCESS_CODE:
        return "ok"
    return "fail"


# ---------------------------------------------------------------------------
# 工作流构建
# ---------------------------------------------------------------------------
def build_flow(chat_model: Any) -> Workflow:
    """构建智能客服·离退休提取流程(含问题分类 + 阶段状态机)."""
    wf = Workflow(name="withdrawal_customer_service")

    # ① 问题分类(需求第 1 点: 首个决策节点为问题分类; 真实 DeepSeek)
    #    取最后 user 消息: 用 IfElse 计算节点(基础节点)承担
    wf.add_node(IfElse(name="last_user", condition=_last_user_text))
    wf.add_node(
        QuestionClassify(
            name="classify",
            chat_model=chat_model,
            categories=[
                {"name": "离退休提取", "description": "用户咨询公积金离退休提取相关业务"},
                {"name": "其他", "description": "其他公积金问题"},
            ],
            query=wf.ref("last_user.branch"),
            description="判断用户是否咨询公积金离退休提取",
        )
    )

    # ② 联合路由: 先分类, 再按 退出/阶段变量/分类结果 分发
    wf.add_node(IfElse(name="route", left=wf.ref("classify.branch"), condition=_route_flow))
    # 离退休提取 → 初始化流程: nextStage=start
    wf.add_node(VariableUpdate(name="init_flow", key="nextStage", value="start"))
    wf.add_node(VariableOutput(name="init_out", variables={"nextStage": None}))
    # 其他 → 公积金知识回答(真实 DeepSeek, 流式输出)
    wf.add_node(
        ChatLLM(
            name="kb_answer",
            chat_model=chat_model,
            system="你是公积金客服助手, 请针对公积金知识(含离退休提取)给出专业、简洁的回答。",
            output=True,
        )
    )

    # ③ 阶段 start → nextStage=confirmSocialSecurityCard
    wf.add_node(
        VariableUpdate(name="stage_start", key="nextStage", value="confirmSocialSecurityCard")
    )
    wf.add_node(VariableOutput(name="start_out", variables={"nextStage": None}))

    # ④ 阶段 confirmSocialSecurityCard: 携带 flag 调第三方 HTTP
    #    读 flag 变量: 用 IfElse 计算节点(基础节点)承担
    wf.add_node(IfElse(name="flag", condition=_flag_value))
    wf.add_node(
        HttpRequest(
            name="http",
            url=THIRD_PARTY_URL,
            method="POST",
            body={"flag": wf.ref("flag.branch")},
        )
    )
    wf.add_node(IfElse(name="http_judge", left=wf.ref("http.json"), condition=_http_ok))
    # 成功 → nextStage=withdrawalCard
    wf.add_node(VariableUpdate(name="succ_flow", key="nextStage", value="withdrawalCard"))
    wf.add_node(VariableOutput(name="succ_out", variables={"nextStage": None}))
    # 失败 → 返回第三方报错文案(data.respMsg) + errExit=True
    #    取 respMsg: 用 IfElse 计算节点(基础节点)承担
    wf.add_node(IfElse(name="resp", left=wf.ref("http.json"), condition=_resp_msg))
    wf.add_node(VariableUpdate(name="err_flag", key="errExit", value=True))
    wf.add_node(VariableOutput(name="err_out", variables={"errExit": None}))
    wf.add_node(Answer(name="err_ans", text=wf.ref("resp.branch")))

    # ⑤ 阶段 userInput: UserInput 暂停让用户输入 → 更新 inputStr 回传前端
    wf.add_node(UserInput(name="ask_input", description="请输入您的补充信息"))
    wf.add_node(VariableUpdate(name="set_input", key="inputStr", value=wf.ref("ask_input.input")))
    wf.add_node(VariableOutput(name="input_out", variables={"inputStr": None}))

    # ⑥ 结束/退出共用流程: 单节点清空其余变量为空串 + 返回 nextStage=end
    #    显式传值(网关按请求基线 merge_new_variables 只回传变更键),
    #    无需 VariableUpdate 清空链
    wf.add_node(
        VariableOutput(
            name="end_out",
            variables={
                "currentStage": "",
                "flag": "",
                "exitBusiness": "",
                "errExit": "",
                "nextStage": "end",
            },
        )
    )

    # ⑦ 连线
    wf.add_edge("last_user", "classify")  # 问题分类(第一决策节点)
    wf.add_edge("classify", "route")  # 分类结果 → 联合路由
    wf.add_conditional_edge(
        "route",
        {
            "exit": "end_out",  # exitBusiness=true → 清空变量 + nextStage=end
            "start": "stage_start",  # currentStage=start → 阶段推进
            "confirm": "flag",  # currentStage=confirmSocialSecurityCard → 第三方校验
            "withdraw": "end_out",  # currentStage=withdrawalCard → 清空变量 + nextStage=end
            "input": "ask_input",  # currentStage=userInput → 交互输入
            "init": "init_flow",  # 离退休提取(初始) → nextStage=start
            "kb": "kb_answer",  # 其他 → 公积金知识回答
        },
    )
    wf.add_edge("init_flow", "init_out")
    wf.add_edge("init_out", "__end__")
    wf.add_edge("kb_answer", "__end__")

    wf.add_edge("stage_start", "start_out")
    wf.add_edge("start_out", "__end__")

    wf.add_edge("flag", "http")
    wf.add_edge("http", "http_judge")
    wf.add_conditional_edge("http_judge", {"ok": "succ_flow", "fail": "resp"})
    wf.add_edge("succ_flow", "succ_out")
    wf.add_edge("succ_out", "__end__")
    wf.add_edge("resp", "err_flag")
    wf.add_edge("err_flag", "err_out")
    wf.add_edge("err_out", "err_ans")
    wf.add_edge("err_ans", "__end__")

    # userInput 交互链: 用户输入 → 更新 inputStr → 回传前端
    wf.add_edge("ask_input", "set_input")
    wf.add_edge("set_input", "input_out")
    wf.add_edge("input_out", "__end__")

    # 结束/退出共用: 单节点清空变量并返回 nextStage=end
    wf.add_edge("end_out", "__end__")

    return wf


# ---------------------------------------------------------------------------
# 模拟第三方服务(演示 HTTP 成功/失败分支, 非业务必需)
# ---------------------------------------------------------------------------
def run_mock_third_party(host: str = "0.0.0.0", port: int = 9080) -> None:
    """启动模拟第三方服务: POST /test1 校验社保卡.

    flag=true  → code=FBASE0000(成功)
    flag=false → code=FBASE9999, data.respMsg=社保卡校验失败(失败)

    Args:
        host/port: 监听地址(默认 0.0.0.0:9080).
    """
    import uvicorn
    from fastapi import FastAPI

    app = FastAPI(title="Mock Third-Party (test1)")

    @app.post("/test1")
    def check_social_security_card(payload: dict[str, Any]) -> dict[str, Any]:
        flag = str(payload.get("flag", "")).lower() == "true"
        if flag:
            return {"code": THIRD_PARTY_SUCCESS_CODE, "data": {"respMsg": "社保卡校验通过"}}
        return {"code": "FBASE9999", "data": {"respMsg": "社保卡校验失败, 请核对信息"}}

    uvicorn.run(app, host=host, port=port)


# ---------------------------------------------------------------------------
# 入口: 启动网关(或模拟第三方)
# ---------------------------------------------------------------------------
def main() -> None:
    """入口: 解析参数后启动网关或模拟第三方服务."""
    import argparse

    parser = argparse.ArgumentParser(description="智能客服·离退休提取流程示例")
    parser.add_argument(
        "--mock-third-party",
        action="store_true",
        help="启动模拟第三方服务(localhost:9080), 而非网关",
    )
    args = parser.parse_args()

    if args.mock_third_party:
        run_mock_third_party()
        return

    api_key = os.environ.get("DEEPSEEK_API_KEY")
    if not api_key:
        raise SystemExit("请设置环境变量 DEEPSEEK_API_KEY 以真实调用 DeepSeek API")

    chat_model = DeepSeekClient(api_key=api_key)
    wf = build_flow(chat_model)
    app = wf.compile()
    print("已编译工作流:", wf.name)
    print("注册应用 appId=app-withdraw, 启动网关 http://0.0.0.0:8080")
    serve({"app-withdraw": app})


if __name__ == "__main__":
    main()

回到示例索引