哈? LLM 的工具调用还能这么玩?! - V2EX
V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
请不要在回答技术问题时复制粘贴 AI 生成的内容
pmpmp
V2EX    程序员

哈? LLM 的工具调用还能这么玩?!

  •  1  
  •   pmpmp 20 小时 45 分钟前 1247 次点击

    前面我们讨论过工具调用的一些文章,比如《 MCP 到底是个什么鬼?》,看过的朋友们似乎已经大概搞清楚了 LLM 的 function calling 的是怎么回事、该怎么用了。你说,function calling 的作用不就这样嘛?还有啥好聊的?

    但是,你有没有想过,它还能用来干啥?你可能觉得我要开始输出什么奇技淫巧了,不不不,我们这个号不会干这种事情,大可以放心食用。

    再开始之前,我们得再回忆一下 function calling 的本质到底是什么?

    你说这还不简单嘛,不就是,让 LLM 输出工具调用,然后 APP 真的去调用工具,然后把结果再给到 LLM 进行更精准的推理么?

    是的,没错,我们先回忆一下这个过程(如下) 一会你会拍大腿的

    1. APP 调用 LLM ,并传入工具的定义 ↓ 2. LLM 返回工具调用的 JSON 描述(工具调用指令) ↓ 3. APP 去调用工具,并得到结果,将结果再次传给 LLM ↓ 4. LLM 根据原始 prompt + 工具结果,推理出结论 ↓ 5. APP 收到结果 

    然后再问自己一个问题:2 中,LLM 返回的工具调用指令我们能不能拿来干点其他的事情呢?

    我们从 instructor 这个库开始聊起吧,Instructor 是一个非常轻量级的库,他的作用是 Structured Outputs for LLMs ,让 LLM 输出结构化的数据,什么意思呢,正常情况下,我们调用 LLM 期望它输出结构化的数据是非常恼火的,有些模型是支持的,但是输出的结果也不一定是准确的,要么是格式问题,要么缺胳膊少腿,虽然你用提示词去约束它,但是它依然是有可能会出错的,Instructor 干了一件非常简单的事情,就是保证 LLM 输出的就是你想要的结构化数据,它的代码大概是这样的(来自它的 Github ):

    import instructor from pydantic import BaseModel # Define what you want class User(BaseModel): name: str age: int # Extract it from natural language client = instructor.from_provider("openai/gpt-4o-mini") user = client.chat.completions.create( response_model=User, messages=[{"role": "user", "content": "John is 25 years old"}], ) print(user) # User(name='John', age=25) 

    他是怎么做到的呢?难道里面偷偷摸摸的套了一个流程,while loop 直到 LLM 输出正确的结构化信息?当然不是了,其实它用了一个 function calling 的 trick ,什么意思呢?

    它是这样做的:首先 User 必须是一个 pydantic 的 BaseModel ,这样,Instructor 就能拿到这个数据结构的 json 描述了,对吧?比如是这样的:

    # Instructor 内部会把它转成这样: function_schema = { "name": "User", "parameters": { "type": "object", "properties": { "name": {"type": "string"}, "age": {"type": "integer"} }, "required": ["name", "age"] } } 

    然后呢?然后它就把这个数据结构“伪装”成一个函数调用,具体怎么做呢?根据不同的 LLM 的数据格式要求,将这个 json 转为一个“函数”传给 LLM ,让 LLM 必须调用这个"函数",比如,当 LLM 是 openai 的时候,他就偷偷的在 tool_choice 里面放上:

    # 先把 Pydantic 模型转换为 tool 定义 new_kwargs["tools"] = [ { "type": "function", "function": {"name": "User", "parameters": {"name": "string", "age": "int"}}, # 关键在这里 } ] # 然后,强制让 LLM 调用这个"函数", new_kwargs["tool_choice"] = { "type": "function", "function": {"name": "User", "parameters": {"name": "string", "age": "int"}}, } 

    “傻乎乎”的 LLM 看到一定要调用这个函数,它就会在推理的过程中输出一个 function calling 的返回,这时候 Instructor 顺其自然的就捕获到 LLM 返回的这个 tool_call 了,比如是这样的:

    "tool_call":{"name": "User", "arguments": '{"name": "John", "age": 25}'} 

    然后呢?然后 Instructor 就自己构建一个 User 的对象再返回给你呗。

    于是,这就是你在开头看到的“魔法”的那一幕 传一个数据的定义,哇塞,它真的就给你返回来了,严丝合缝,不出错,还节省了 token (不然你自己还得反复的 call LLM 对吧?)。

    有意思吧?你看,这个过程是不是就是利用了 function calling 的能力?虽然最后并没有真的去 call 什么函数,但是这个机制是可以被我们用作结构化数据的。Instructor 用了一个“欺骗”LLM 的办法拿到了自己想要的东西,

    你说,这不就是个雕虫小技么?登不上什么大雅之堂吧?

    呵呵,其实有不少著名的框架里面的一些巧思其实都是这么做的,我再给你举几个例子吧。

    Langchain 不久前发布了 V1 版本,其中有一个重要的更新就是:Sructured output

    from langchain.agents import create_agent from langchain.agents.structured_output import ToolStrategy from pydantic import BaseModel class Weather(BaseModel): temperature: float condition: str def weather_tool(city: str) -> str: """Get the weather for a city.""" return f"it's sunny and 70 degrees in {city}" agent = create_agent( "gpt-4o-mini", tools=[weather_tool], response_format=ToolStrategy(Weather) ) result = agent.invoke({ "messages": [{"role": "user", "content": "What's the weather in SF?"}] }) print(repr(result["structured_response"])) # results in `Weather(temperature=70.0, cOndition='sunny')` 

    他是怎么做的?有兴趣的去看下它的代码,其实和 Instructor 的做法几乎如出一辙。这里稍微吐槽一下(其实社区里面都有这样的吐槽),Langchain 的这个接口设计的多少是有点“业余”的 你发现没,tool 和 response_format 并没有什么对应关系,如果我传了多个 tool 和多个 response_format 呢?我怎么知道里面是怎么处理的?最后会返回什么给我?站在开发者的角度看,很晦涩很不透明。我估计后面还得改进。

    我们再举一个例子,比如大名鼎鼎的 autoGen ,也用了这个“小技巧”。

    autoGen 里面的多智能体合作是怎么实现的呢?难道真的想像营销号说的那样,框架实现了让多个智能体在里面“群聊”么?

    当然不是的,这都是营销话术,真正是怎么实现的呢?还是用 function calling 的 trick ,这个过程大概是这样的:

    首先,autoGen 的 AssistantAgent 有一个参数叫做:handoffs ,虽然你可能不怎么会用到它,这是什么东西呢,其实本质上它就是描述了“在何种情况下将发言权转移给哪个 Agent”,所谓的发言权也是个营销话术,其实就是 autoGen 的调度引擎决定运行哪个 agent ,这是第一步

    然后,autoGen 的 Swarm (就是负责调度的)就开始“演戏了”,当某一个 agentA 被调度起来的时候(内部就是一个 ReAct ),它一定会去跟 LLM 交互吧,关键来了,跟 LLM 交互的时候,它偷偷的将 handoffs 这种描述包装成了一个“假函数”传给 LLM ,例如函数名叫 xxx ,描述是“假设遇到这样这样的情况,请调用该函数,参数是 AgentB”,LLM 一看,哦,现在是这种情况,所以我要调用 xxx 函数,于是,这个 xxx 的调用指令就被 Swarm 捕捉到了,然后它一看,LLM 上套了,那么我们现在就要转而去调度 B 智能体,你看,本质上就是用“欺骗”LLM 的办法,让 LLM 通过 function calling 做了一次路由的调度,是不是很巧妙?

    整个过程的基本思想就是这样的:

    # 给 LLM 看到的"工具": tools = [ { "name": "calculator", # 真工具 "description": "计算数学表达式" }, { "name": "transfer_to_agent_BBB", # 假工具 "description": "转交给 agent_BBB" }, { "name": "transfer_to_agent_CCC", # 假工具 "description": "转交给 agent_CCC" } ] # LLM 以为自己在"调用工具" # 实际上是在"做路由决策" 

    所以,哪里有什么“群聊”?表面上看起来是多个 Agent 在"讨论",实际上呢,都是Swarm耍的花招,让 LLM 被忽悠的在后面吭哧吭哧的干活,Swarm 在前面出尽了风头,哈哈。

    类似的例子还有很多,比如 agno 这个框架,也在用这样的方式刷花招,agno 里面有一个东西叫做 ReasoningTools,看起来也是一个很神奇的东西,仿佛加上这个参数,LLM 就能进行“深度思考”了,他是怎么做到的呢?也是用了 function calling 的原理来忽悠“老实巴交”的 LLM 进行中间过程的输出,有兴趣的小伙伴自己去探索一下吧,评论区见,哈哈哈。

    好啦,今天就聊到这里吧,Agent 的领域其实很多东西并没有大家想的那么神奇,都是在工程上利用了 LLM 的特性和机制做了事情,有些事情很有趣,比如我们今天讨论的,绝大部分事情都是苦哈哈的事情。

    以上,全文。感谢大家

    最后再为自己写的一个框架做个广告,求一波啊大神们

    chak ( https://github.com/zhixiangxue/chak-ai ),一个极简的 LLM 调用工具,轻量级,内置上下文管理和工具调用,使用起来非常简单、顺手、优雅。

    点它点它点它点它

    7 条回复    2025-11-28 18:49:35 +08:00
    mooncakeSec
        1
    mooncakeSec  
       20 小时 19 分钟前
    模型支持的 Sructured output 和 function call 不一样,如果模型支持就不要用 function call 了
    neteroster
        2
    neteroster  
       20 小时 5 分钟前 via Android
    其实 function call 或者 structure output 区别没那么大,推理后端没做约束解码的话,function call 的参数也不能保证准确... 做了约束解码的话,structure output 和 function call 都是保证准确的。

    当然,唯一的例外的是,部分提供商只做了 function call ,或者只有 function call 用了约束解码
    pmpmp
        3
    pmpmp  
    OP
       20 小时 4 分钟前
    @mooncakeSec 嗯是的,所以一般框架里面都会做 fallback ,支持的就直接用,不支持的 LLM 框架他们会这样做
    flyme2them00n
        4
    flyme2them00n  
       19 小时 35 分钟前   1
    已 star ,希望多发一些这类型的文章
    andyskaura
        5
    andyskaura  
       19 小时 24 分钟前
    @pmpmp 估计 Sructured output 的实现方式本质上和 function call 差不多的 ,要严格准确的格式输出对于 llm 来说有点辛苦了,像之前的 deepseek 的 json output ,总是给我少几个大括号。
    uncleroot
        6
    uncleroot  
       18 小时 24 分钟前
    点赞!挺有意思
    kulove
        7
    kulove  
       18 小时 20 分钟前 via Android
    以后模型应该都会支持结构化输出的
    关于     帮助文档     自助推广系统     博客 &nbs;   API     FAQ     Solana     2575 人在线   最高记录 6679       Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 20ms UTC 05:10 PVG 13:10 LAX 21:10 JFK 00:10
    Do have faith in what you're doing.
    ubao msn snddm index pchome yahoo rakuten mypaper meadowduck bidyahoo youbao zxmzxm asda bnvcg cvbfg dfscv mmhjk xxddc yybgb zznbn ccubao uaitu acv GXCV ET GDG YH FG BCVB FJFH CBRE CBC GDG ET54 WRWR RWER WREW WRWER RWER SDG EW SF DSFSF fbbs ubao fhd dfg ewr dg df ewwr ewwr et ruyut utut dfg fgd gdfgt etg dfgt dfgd ert4 gd fgg wr 235 wer3 we vsdf sdf gdf ert xcv sdf rwer hfd dfg cvb rwf afb dfh jgh bmn lgh rty gfds cxv xcv xcs vdas fdf fgd cv sdf tert sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf sdf shasha9178 shasha9178 shasha9178 shasha9178 shasha9178 liflif2 liflif2 liflif2 liflif2 liflif2 liblib3 liblib3 liblib3 liblib3 liblib3 zhazha444 zhazha444 zhazha444 zhazha444 zhazha444 dende5 dende denden denden2 denden21 fenfen9 fenf619 fen619 fenfe9 fe619 sdf sdf sdf sdf sdf zhazh90 zhazh0 zhaa50 zha90 zh590 zho zhoz zhozh zhozho zhozho2 lislis lls95 lili95 lils5 liss9 sdf0ty987 sdft876 sdft9876 sdf09876 sd0t9876 sdf0ty98 sdf0976 sdf0ty986 sdf0ty96 sdf0t76 sdf0876 df0ty98 sf0t876 sd0ty76 sdy76 sdf76 sdf0t76 sdf0ty9 sdf0ty98 sdf0ty987 sdf0ty98 sdf6676 sdf876 sd876 sd876 sdf6 sdf6 sdf9876 sdf0t sdf06 sdf0ty9776 sdf0ty9776 sdf0ty76 sdf8876 sdf0t sd6 sdf06 s688876 sd688 sdf86