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

查看 48|回复 5
作者:pmpmp   
前面我们讨论过工具调用的一些文章,比如《 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 调用工具,轻量级,内置上下文管理和工具调用,使用起来非常简单、顺手、优雅。

☝️☝️☝️☝️☝️☝️☝️☝️点它点它点它点它☝️☝️☝️☝️☝️☝️☝️☝️

llm, function, calling, tools

mooncakeSec   
模型支持的 Sructured output 和 function call 不一样,如果模型支持就不要用 function call 了
neteroster   
其实 function call 或者 structure output 区别没那么大,推理后端没做约束解码的话,function call 的参数也不能保证准确... 做了约束解码的话,structure output 和 function call 都是保证准确的。
当然,唯一的例外的是,部分提供商只做了 function call ,或者只有 function call 用了约束解码
pmpmp
OP
  
@mooncakeSec 嗯是的,所以一般框架里面都会做 fallback ,支持的就直接用,不支持的 LLM 框架他们会这样做
flyme2them00n   
已 star ,希望多发一些这类型的文章
andyskaura   
@pmpmp 估计 Sructured output 的实现方式本质上和 function call 差不多的

,要严格准确的格式输出对于 llm 来说有点辛苦了,像之前的 deepseek 的 json output ,总是给我少几个大括号。
您需要登录后才可以回帖 登录 | 立即注册

返回顶部