在涉及资金处理的场景中,应由代码决定工具调用序列而非LLM自主选择,以退款场景为例说明两种架构的边界。
Originally published on the Ramsud Technologies blog.

AI 系统设计的核心问题不是"要不要用 LLM",而是:谁来决定系统调用哪些工具?
在 Agentic 系统中,LLM 会看到一组可用工具,然后决定以什么顺序、传什么参数来调用它们。在确定性系统中,代码决定工具的调用顺序,LLM 根本看不到这些工具。
Agentic: LLM sees tools → LLM decides which to call → execution follows
Deterministic: Code decides tool sequence → tools execute → no LLM in the decision loop*
Agentic 的方案对很多工作流都很有吸引力——它能处理你没有写过的边缘情况,能读取混乱的自然语言,能适应新场景。对于分类、草稿生成、摘要总结这类任务,通常是正确的选择。
但对于退款,我们选择了确定性方案。原因如下,以及我们在这条路上的一些感悟。
退款的判定是非此即彼且影响重大的:钱要么从公司账户流出去,要么不流出。需要考虑的因素——配送状态、距交付天数、订单金额、客户历史——全是结构化数据。不存在需要解读的杂乱文本。而且一旦出了问题,不是"体验稍微变差"那么简单,而是:
所以我们用了最朴素的方式:固定顺序的数据查询、对政策规则的确定性资格校验、硬编码的决策路径。永远不会有模型来决定钱是否流动。
我们遵循的规则是:LLM 可以为关于钱的决策提供信息,但不应该独自做出决策。
在 Agentic 模式下,LLM 能看到你的工具并决定调用哪些。对于退款系统,这意味着模型在决定"我现在应该调用 issue_refund() 吗,还是先收集更多信息?",或者"这个客户的语气表明他们应该得到退款"。
问题在于:第一天,用一种措辞,LLM 按顺序 A 调用工具。第二天,语境稍有不同,它按顺序 B 调用。决策不可复现、不可审计、无法解释。
我们借鉴了 Agent 基础设施的模式,但去掉了自主权。我们构建了一个作用域受限的工具网关,它:
这是防御性基础设施,不是 AI 自主权。网关不决定何时调用工具——由代码决定。LLM 从不与网关对话,由代码来对话。
我们最初给它起名叫 SecureMCPGateway,暗示它符合 Model Context Protocol 协议。其实并不是——它只是一个硬编码的调度器,只有三个允许的函数名,没有动态发现。我们把它改名为 ScopedToolGateway。更清晰,也更诚实地反映了它的实际功能。
经验教训:采用真正重要的基础设施模式(审计日志、作用域边界、访问控制)。拒绝那些赋予 LLM 不应有的自主权的框架模式。
随着系统演进,LLM 最自然的位置是在上游:把自由文本的支持请求解析为结构化分类、起草一封比模板更友好的拒绝邮件、在请求确实模糊不清时判断应该适用哪条政策。
所有这些都是辅助性质。最终通向钱流动的那一步仍然由人或确定性规则来把关。模型让人类的决策更快,而不是取代它。
陷阱:因为模型顺手就用,而不是因为你真的需要它。
1. 解析输入 — 从自由文本请求中提取客户 ID 和订单 ID。此时 LLM 未参与。
2. 按固定顺序调用工具 — 代码决定调用 get_customer() → get_order() → get_delivery()。每次顺序一致。
3. 作用域网关验证 — 每次调用都用 token 认证。不能调用其他工具。不允许动态发现。
4. 策略查询 — 代码查询策略规则并返回资格状态和时间窗口。仅为参考,不做决策。
5. 确定性决策 — 固定规则:IF status IN [damaged, lost] AND days_since_delivery <= 30 THEN eligible ELSE decline。二值逻辑,无模型判断。
6. 创建工单 — 符合条件 → pending_approval;不符合 → declined(自动处理)。
7. 人工审批 — 支持人员审核工单并调用 approve_refund()。人类做出移动资金的实际决策。
8. 退款发放 — 数据库约束防止同一订单重复退款。所有操作均记录日志。
两道关卡,而非一道:代码决定资格,人类决定审批。两道关卡都不是 LLM。
第 8 步的数据库约束比听起来更重要——一个朴素的事等性检查(内存中的集合、工单上的状态标志)在单线程的正常路径测试中能通过,但在并发下仍然会让两笔退款漏过去,因为大多数实现在错误的主体上执行了唯一性约束。约束必须落在订单上,而不是工单上,因为每次请求都会创建新的工单:
CREATE UNIQUE INDEX one_issued_refund_per_order ON support_tickets (customer_id, order_id) WHERE status = 'refund_issued';
一个针对 status = 'refund_issued' 的部分唯一索引,意味着无论创建了多少工单、有多少进程在竞态,Postgres 物理上不可能为同一订单持有两条已发放退款的记录。
LLM 从模式中学习——如果它看到"客户打了 3 次电话 → 退款获批",它可能学会"多打几次电话就能拿到退款"。规则引擎不会学习,它只做你告诉它的事。
"配送状态为损毁且在 30 天内"是对"为什么这笔退款发生了"的合理回答。"模型认为语气表明客户很不满"不是。
LLM 在无法自信处理的边缘情况下需要人类介入。确定性系统天生就将边缘情况路由到人工工单——结果相同,但失败路径是可见的且被记录的,不是事后才发现的。
在为一个工作流启用 Agent 框架之前,先问自己:
如果最后一个问题的诚实答案是"更厉害",那你是在为一个没有能力收益的工作流增加攻击面和失败模式。经验证、可审计的确定性代码,优于判断无法完全验证的模型——特别是涉及真钱、PII 或不可逆操作的场景。
我们宁愿交付一个能被证明正确的朴素版本,而不是一个看起来很酷但可能没问题的版本。
这不是一篇回顾文章——而是一个正在进行中的项目。我们正在生产环境中构建这个退款 Agent,并在做出决策的同时记录架构思路。如果你也在为一个涉及金钱的工作流权衡 LLM 与确定性方案的取舍,完整的技术文章里有生产级模式的其他部分(审计日志设计、人工审批流程以及合规角度)。