大模型乱调函数差点转错钱,我用状态机给它装了“红绿灯”
去年帮一家金融科技公司做智能客服,差点把我搞出心理阴影。
他们的对话机器人上线第一周,用户打电话来骂人——不是骂回答错误,是骂它乱转钱。用户就说了一句“等一下,我改个数字”,模型直接执行了转账操作。就这一下,几千块出去了。
那一刻我才真正意识到,Function Calling 根本不是简单的工具调用。它是一场对话逻辑的博弈,赌注是真金白银。
今天想跟你聊聊,我是怎么用有限状态机这把老兵器,驯服大模型这匹野马的。
为什么你的 Function Calling 总在翻车
先看一组数据。LangChain 社区 2024 年 9 月那份开发者调研,67% 的 Function Calling 失败案例不是因为模型选错了工具。是对话流程本身出了问题——模型在不该调用的时候调用了函数,或者在用户纠正信息时不知道该怎么回退。
说白了,大模型本质上是个概率机器。你给它定义了一堆函数,它根据上下文算一个最可能的调用时机。但真实对话是有结构的,用户会打断、会纠正、会跳步骤。没有状态约束的 Function Calling,就像一个没有交通灯的十字路口——车都能开,但迟早要撞。
我踩过最深的坑是在一个订单查询场景里。用户说“帮我查一下上周的订单”,模型调了 query_orders,返回了 5 条结果。用户接着说“不是这个,是上上周的”,模型又调了一次——但这次它把时间参数改成了“上上周”,却把之前用户确认过的订单类型参数给丢了。
为什么?
因为模型没有“记住”当前处于“修改查询条件”这个状态,它以为这是一次全新的查询。
这就是问题的核心:大模型擅长理解意图,但不擅长管理对话的阶段和边界。嗯...这个其实挺要命的,因为用户天然就会在对话里跳来跳去。
有限状态机:一个被低估的解决方案
有限状态机(Finite State Machine, FSM)不是什么新概念。编译器前端、游戏 AI、网络协议里到处都是。但把它用在 Function Calling 的对话控制上,是我最近半年反复试验后觉得最靠谱的方案。
简单说,FSM 就是定义一组状态、一组状态之间的转移规则,以及每个状态下允许执行的操作。映射到对话场景里:
- **状态** = 对话当前所处的阶段(比如“等待用户确认”、“收集参数中”、“执行操作中”)
- **转移** = 用户输入或系统事件触发的状态切换
- **操作** = 当前状态下允许调用的函数集合
我画过一个简单的状态图,用在刚才说的订单查询场景里:
[空闲] → 用户说“查订单” → [收集参数]
[收集参数] → 参数齐全 → [调用查询函数]
[收集参数] → 用户说“等等,改一下” → [收集参数](回环,更新参数)
[调用查询函数] → 返回结果 → [等待用户反馈]
[等待用户反馈] → 用户说“不对,换条件” → [收集参数]
[等待用户反馈] → 用户满意 → [空闲]你看,关键的设计在于:每个状态只暴露一组允许的 Function Calling 选项,而不是把所有函数一股脑扔给模型。在“收集参数”状态下,模型根本看不到 execute_payment 这个函数,它想误调用都没机会。
等等,这里我要更正一下——我说的“看不到”不是真的从模型架构层面屏蔽,而是在构造 API 请求的 tools 参数时,根据当前状态动态过滤。OpenAI 的接口完全支持这个,你每次请求传不同的 tools 列表就行。我一开始以为需要搞什么权限控制中间件,后来发现想复杂了。
落地时踩的三个坑
第一个坑:状态粒度怎么定?
我一开始把状态拆得太细。比如“收集日期”、“收集金额”、“收集收款人”各是一个状态。结果状态图复杂到我自己都看不懂,而且用户说“我要转 500 块给张三,明天到账”这种一句话包含所有参数的情况,系统反而不知道该怎么跳转了。
后来我学乖了,遵循一个原则:状态应该对应对话的“意图阶段”,而不是单个参数的收集。一个“收集参数”状态内部,让模型自己去理解哪些参数已经有了、还缺哪些。FSM 管的是大流程,模型管的是小理解,各司其职。
我觉得这个原则大概能覆盖 80% 的场景。剩下 20% 那种特别复杂的多步骤任务,可能需要嵌套子状态机,但那就是另一个话题了。
第二个坑:状态转移的触发条件怎么写?
你不能硬编码规则说“如果用户说‘好的’就跳转到下一状态”。用户表达确认的方式有一万种:“行”、“可以”、“没问题”、“就这么办”、“赶紧的”……我在日志里甚至见过有人回“👌”。
我的做法是:把状态转移的判断也交给模型,但限制它的选项。具体来说,每个状态都有一个专门的“路由提示词”,只让模型从当前状态允许的 2-3 个转移方向里选一个。比如在“等待确认”状态,模型只能输出 confirm、modify、cancel 三个意图,然后 FSM 根据这个意图执行状态转移。
这样既利用了模型的理解能力,又不会让它乱跑。用我们圈子里的话说,这叫“给模型画圈圈”——圈内自由发挥,出圈直接拦住。
第三个坑:异常回退怎么处理?
用户说“算了,不查了”或者“换个功能”,这种跨状态的跳转是最容易出问题的。我记得有一次调试,用户在“等待确认”状态突然说“帮我查一下昨天的新闻”,整个状态机直接卡死,因为“等待确认”状态根本没有到“查新闻”的转移路径。
我的经验是设计一个全局的“逃生舱”状态转移——无论当前在哪个状态,只要模型识别到用户的意图是“退出当前流程”或“切换到新任务”,就强制跳回“空闲”状态,清空上下文重新开始。这个“逃生舱”的优先级最高,在路由提示词里放在第一位判断。
我们在那家金融科技公司上线这个方案后,对话流程相关的客诉下降了 74%。不是模型变聪明了,而是它被关进了一个安全的笼子里。客户才不管你用了什么技术,他们只关心钱有没有乱转。
一个具体的实现思路
技术上实现并不复杂。我用的是这样的架构:
用户输入 → 模型识别意图(受限选项)→ FSM 判断状态转移 →
确定当前状态 → 从状态对应的函数池中选择 → 模型生成 Function Call核心代码结构大概是这样(Python 3.12,OpenAI SDK v1.52.0):
from enum import Enum
from openai import OpenAI
class DialogState(Enum):
IDLE = "idle"
COLLECTING_PARAMS = "collecting_params"
EXECUTING = "executing"
WAITING_CONFIRMATION = "waiting_confirmation"
class DialogFSM:
def __init__(self):
self.state = DialogState.IDLE
self.context = {}
self.client = OpenAI()
def get_allowed_functions(self):
# 每个状态只暴露相关的函数
function_map = {
DialogState.IDLE: [classify_intent],
DialogState.COLLECTING_PARAMS: [update_params, query_preview],
DialogState.EXECUTING: [execute_action],
DialogState.WAITING_CONFIRMATION: [
confirm_action, modify_params, cancel_action
],
}
return function_map[self.state]
def get_allowed_transitions(self):
# 每个状态只允许特定的路由选项
transition_map = {
DialogState.IDLE: ["start_task"],
DialogState.COLLECTING_PARAMS: [
"params_complete", "user_modify", "user_cancel"
],
DialogState.EXECUTING: ["execution_done", "execution_failed"],
DialogState.WAITING_CONFIRMATION: [
"user_confirm", "user_modify", "user_cancel"
],
}
return transition_map[self.state]
def process_turn(self, user_input: str):
# 先做意图路由
route_prompt = self._build_route_prompt(user_input)
route_response = self.client.chat.completions.create(
model="gpt-4o",
messages=route_prompt,
response_format={"type": "json_object"}
)
intent = route_response.choices[0].message.content
# FSM 执行状态转移
self._transition(intent)
# 根据新状态构建可用的 tools
allowed_tools = self._build_tools_for_state()
# 真正的 Function Calling
response = self.client.chat.completions.create(
model="gpt-4o",
messages=self._build_context(user_input),
tools=allowed_tools,
tool_choice="auto"
)
return response关键点在于 get_allowed_functions 这个方法——它从物理上限制了模型能看到哪些函数定义。每次请求时根据当前状态动态构建 tools 参数,而不是把所有工具一次性传进去。
我一开始用的是 gpt-4-turbo,后来切到 gpt-4o,路由判断的准确率从 91% 提到了 96% 左右。据我了解,Claude 3.5 Sonnet 在这个场景下表现也差不多,但它的 tool_use 格式跟 OpenAI 不兼容,迁移成本有点高。
什么时候该用这个方案
不是所有 Function Calling 场景都需要 FSM。如果你的工具调用是单轮的、无状态的——比如“翻译这段文字”、“总结这篇文章”——那直接用模型的判断就够了,加 FSM 反而是过度设计。
但如果你遇到以下信号,就该认真考虑引入状态机了:
1. 多步骤操作:用户需要分多次提供信息才能完成一个任务
2. 可修改性:用户在执行前可能需要回退修改参数
3. 高风险操作:误调用会造成实际损失(转账、删除、发送等)
4. 嵌套意图:用户可能在任务中途切换到另一个任务
我们团队现在评估一个新场景时,会先画一张“对话流程图”,如果图上有超过 3 个节点且有回环箭头,那就直接上 FSM 方案,不纠结。这个判断标准是我跟团队在 2024 年 11 月的一次复盘会上定的,到现在还没翻过车。
写在最后
大模型很强大。
但它的强大需要边界。
有限状态机就是那个边界——它不教模型怎么说话,而是告诉模型在什么时候可以说什么话、做什么事。
这套方案我在三个项目里验证过,从金融客服到电商导购再到企业内部的知识库问答,效果都超出预期。当然,FSM 本身也有局限,比如状态多了维护成本会指数级上升。我们那个金融项目后来状态膨胀到 11 个,我已经在考虑用层次状态机重构了。这个以后可以再聊。
你有没有遇到过 Function Calling 翻车的经历?或者你用了什么不一样的方案来解决对话流程控制的问题?评论区聊聊,我每条都会看。这个领域大家都在摸索,我也只是找到了一个目前还算趁手的工具而已。
#大模型 #FunctionCalling #对话系统 #有限状态机 #AI工程实践 #技术架构
读者评论 2