从 Function Calling 到确定性编排:生产级 AI Agent 工具调用层的工程化

过去一年,Agent 从 Demo 走向生产,最大的拦路虎往往不是模型能力,而是"工具调用层"的工程化。很多团队在原型阶段用一段 while True 套着 LLM 就能跑通演示,一到线上就出现参数类型错、工具超时、无限循环、成本失控。本文从协议本质讲起,给出一个可落地的工具调用运行时设计,并讨论什么时候该用 Agent、什么时候不该用。

一、Function Calling 不是魔法,是结构化输出

所谓 Function Calling,本质是让模型在生成自然语言之外,额外输出一段符合 JSON Schema 的结构化指令。模型并不真的"调用"函数——它只负责决定"要调哪个工具、参数填什么",真正的执行由你的代码完成。

{
  "tools": [{
    "name": "query_order",
    "description": "根据订单号查询订单状态与物流信息",
    "parameters": {
      "type": "object",
      "properties": {
        "order_id": {"type": "string", "description": "订单号,形如 ORD-2026-xxxx"}
      },
      "required": ["order_id"]
    }
  }]
}

实战观点:工具的 description 比你想的重要得多。模型靠它做选择,描述模糊("获取信息")会让模型在多个工具间随机摇摆。好的描述是"动词 + 宾语 + 边界条件",例如"根据订单号查询订单状态与物流信息,仅支持 2026 年后的订单"。

二、最小可用工具运行时

下面约 80 行实现一个不依赖任何框架的运行核心:工具注册、模型返回解析、执行、结果回填。

import json
from dataclasses import dataclass
from typing import Callable

@dataclass
class Tool:
    name: str
    description: str
    func: Callable
    schema: dict

class ToolRegistry:
    def __init__(self):
        self._tools = {}
    def register(self, func, schema):
        self._tools[func.__name__] = Tool(
            name=func.__name__,
            description=schema["description"],
            func=func, schema=schema)
    def to_openai_spec(self):
        return [{"type": "function",
                 "function": {"name": t.name,
                              "description": t.description,
                              "parameters": t.schema["parameters"]}}
                for t in self._tools.values()]

registry = ToolRegistry()

def tool(schema):
    def deco(f):
        registry.register(f, schema); return f
    return deco

@tool({"description": "根据订单号查询订单状态",
       "parameters": {"type": "object", "properties": {
           "order_id": {"type": "string"}}, "required": ["order_id"]}})
def query_order(order_id: str) -> dict:
    # 这里接入真实订单服务
    return {"order_id": order_id, "status": "shipped", "eta": "2026-09-30"}

MAX_TURNS = 8

def run_agent(user_msg: str, llm_call) -> str:
    messages = [{"role": "user", "content": user_msg}]
    for _ in range(MAX_TURNS):
        resp = llm_call(messages, tools=registry.to_openai_spec())
        if not resp.tool_calls:
            return resp.content
        for call in resp.tool_calls:
            fn = registry._tools[call.name].func
            args = json.loads(call.arguments)
            result = fn(**args)
            messages.append({"role": "tool",
                             "tool_call_id": call.id,
                             "content": json.dumps(result, ensure_ascii=False)})
    return "超过最大轮次,未能完成"

这个骨架的关键洞见:工具执行是纯函数式的——输入参数、输出结果,不碰全局状态。这样可测试、可重试、可并行。注意 MAX_TURNS 这个看似不起眼的常量,它是防止循环爆炸的第一道闸门。

三、确定性编排 vs 自由规划

Agent 有两种主流形态:

  • ReAct / 自由规划:模型自己决定下一步调哪个工具、循环几次。灵活,适合探索性任务(如"帮我调研竞品并写报告"),但路径不可预测、成本难控。
  • 确定性 DAG 编排:你把流程写成固定图(A→B→C),只在节点内部用 LLM。可观测、可回放、成本固定,适合流程明确的业务(如"下单→风控→通知")。

实战观点:80% 的企业场景应该用确定性编排,而不是自由 Agent。 自由 Agent 的"涌现能力"在 Demo 里很亮眼,在 SLA 里很致命。先写死流程、把 LLM 当局部增强,等流程稳定了再考虑放开。工具调用层真正的价值,是让两种形态共享同一套工具注册与执行底座。

四、生产级必须补的三个坑

1. 参数校验失败。 模型偶尔会输出 order_id=123(数字)而非字符串,或漏掉 required 字段。务必做"宽容解析 + 失败回退":把校验错误作为消息回填给模型让它重试,而不是直接抛异常中断。

def coerce_args(raw: dict, schema: dict) -> dict:
    props = schema["parameters"]["properties"]
    out = {}
    for k, v in props.items():
        val = raw.get(k)
        if v.get("type") == "string" and val is not None:
            out[k] = str(val)          # 数字/布尔宽容转字符串
        else:
            out[k] = val
    return out

def safe_invoke(fn, raw_args, schema, max_retry=2):
    last_err = None
    for _ in range(max_retry + 1):
        try:
            return {"ok": True, "data": fn(**coerce_args(raw_args, schema))}
        except (TypeError, ValueError) as e:
            last_err = str(e)
    return {"ok": False, "error": last_err}

2. 工具超时与重试。 任何外部工具都要设 timeout 与幂等重试。非幂等写操作(如"下单")绝不能盲重试,必须带去重 key,否则一次网络抖动可能重复扣款。

3. 循环爆炸。 自由 Agent 可能陷入"调工具→看结果→再调同一工具"的死循环。除了 MAX_TURNS 硬上限,还应检测"连续相同工具调用"并提前终止,或在工具结果里注入计数器提示模型收敛。

五、什么时候不该用 Agent

  • 输入输出结构固定、规则明确 → 直接写代码/工作流,别上 LLM。
  • 单次问答就能解决 → 普通 RAG 足够。
  • 要求 100% 可审计、零幻觉 → 用规则引擎,Agent 只做建议,不做决策闭环。

Agent 的甜区是:任务路径长、分支多,但每一步都可被工具约束。 用工具把模型的"想象力"框在安全的围栏里,才是生产级 Agent 的精髓——Function Calling 是入口,确定性编排是护栏,而工程化的工具调用层,是把两者粘在一起的水泥。

本文为 ybb.press 技术频道自动发文系列之一。代码均为可运行的最小示例,生产环境请补充鉴权、日志、监控与限流。

点赞(0) 打赏

评论列表 共有 0 条评论

暂无评论
立即
投稿

微信公众账号

微信扫一扫加关注

发表
评论
返回
顶部
0.380837s