单轮问答不难,难的是多轮对话。用户先说"我要退货",机器人问"订单号",用户回"123456"——机器人要知道这个"123456"是订单号,而不是凭空理解。这就是对话状态机要解决的问题:记住用户当前在哪一步、收集了哪些信息、下一步该问什么。

参考资料WTAPI框架
接口路径与字段以api文档weiti.apifox.cn 为准。

一、为什么不能只靠LLM上下文

直接把所有历史消息丢给LLM让它自己理解,看似简单,实际有三个问题:token成本随对话轮数线性增长;LLM可能忘记或篡改早期收集的字段;无法精确控制业务流程(比如订单号必须收集齐才能退货)。

正确做法是用显式状态机管理业务流程,LLM只负责理解单轮输入。

二、状态机的数据结构

每个会话维护一个状态对象,按 instanceId:fromWxid 存储:

import json, time

def get_state(instance_id, from_wxid):
    raw = redis.get(f"state:{instance_id}:{from_wxid}")
    return json.loads(raw) if raw else None

def save_state(instance_id, from_wxid, state, ttl=1800):
    state["updated_at"] = int(time.time())
    redis.setex(
        f"state:{instance_id}:{from_wxid}",
        ttl,
        json.dumps(state, ensure_ascii=False),
    )

def init_state():
    return {
        "intent": None,          # 当前意图,如 refund
        "step": None,            # 当前步骤,如 ask_order_id
        "slots": {},             # 已收集的字段
        "history": [],           # 对话历史(供LLM参考)
    }

三、状态流转逻辑

以退货流程为例,定义状态流转:

FLOWS = {
    "refund": {
        "steps": [
            {"name": "ask_order_id", "slot": "order_id",
             "prompt": "请提供订单号"},
            {"name": "ask_reason", "slot": "reason",
             "prompt": "请说明退货原因"},
            {"name": "confirm", "slot": None,
             "prompt": "信息已确认,是否提交?"},
        ]
    }
}

def handle_message(instance_id, from_wxid, content):
    state = get_state(instance_id, from_wxid)

    # 首次对话:识别意图
    if state is None:
        intent = detect_intent(content)  # 关键词或LLM分类
        if intent in FLOWS:
            state = init_state()
            state["intent"] = intent
            state["step"] = FLOWS(intent)["steps"][0]["name"]
            save_state(instance_id, from_wxid, state)
            return FLOWS[intent]["steps"][0]["prompt"]
        return default_reply(content)

    # 流程中:把当前输入填入槽位
    flow = FLOWS[state["intent"]]
    current_step = next(s for s in flow["steps"] if s["name"] == state["step"])
    if current_step["slot"]:
        state["slots"][current_step["slot"]] = content

    # 推进到下一步
    idx = flow["steps"].index(current_step)
    if idx + 1 < len(flow["steps"]):
        state["step"] = flow["steps"][idx + 1]["name"]
        save_state(instance_id, from_wxid, state)
        return flow["steps"][idx + 1]["prompt"]
    else:
        # 流程结束:调用业务接口
        result = submit_refund(state["slots"])
        redis.delete(f"state:{instance_id}:{from_wxid}")
        return f"退货申请已提交,单号{result}"

四、三个工程细节

槽位抽取:用户可能在一条消息里给多个字段(“订单号123,因为质量差”),用LLM做一次槽位抽取比逐轮问更高效,但要校验抽取结果。

状态超时:状态设TTL(如30分钟),超时自动清空。避免用户中途离开后,隔几天回来机器人还在追问退货原因。

中断与切换:用户在流程中问了无关问题(“你们几点下班”),要能识别并临时跳出流程回答,回答完回到原步骤,不要把闲聊内容塞进槽位。

五、LLM的角色边界

状态机管流程,LLM管两件事:意图识别(用户想干什么)和槽位抽取(从这句话里提取字段)。业务推进、字段校验、接口调用全部由代码控制。这样既保留了自然语言理解的灵活性,又保证了业务流程的确定性。


Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐