
◆ 博主名称: QuZhengRong
AI俘虏,样式苦手
目录

上一篇我们演示了如何调用大模型接口,让大模型回答问题,但从 Agnet 视角出发,它还存在一个很明显的问题:只会说,不会做
你让大模型搜集 数据在电脑上新建一个当日营业额汇总表,它是做不到的,它还缺少和外部交互的"手脚"
要让模型能真正去查数据、跑计算、调接口,还得靠这一篇的主角:Function Calling 与结构化输出
一、Function Calling 原理
Function Calling 是大模型的一项关键受控输出能力,其核心作用是对模型的输出范式进行约束,使其能够将自然语言形式的用户需求,映射为结构化的函数调用意图
数学
What does it mean ?举一个例子:你问大模型深圳南山店今天营业额多少,模型并不清楚具体的营收数据,而程序里刚好有一个 get_store_revenue 函数可以从系统查询营业额,那模型就会返回给你一段这样的结构化数据:
{
"name": "get_store_revenue",
"arguments": {
"store_name": "深圳南山店"}
}
程序拿到这段意图后,自己去调对应的函数,再把结果回传给大模型,模型再给出最终回答
模型只判断该不该调工具、调哪个工具、参数填什么,真正的执行权在程序手上。整个链路是这样的:

模型返回的这段意图必须是可解析的结构化数据,不能是一堆自然语言,否则程序无法统一处理,所以 Function Calling 背后还依赖一个能力:结构化输出。让模型按你定的格式返回数据,是这一篇要解决的核心问题
二、工具(Tool)的定义与注册
模型调用工具的前提是知道哪些工具可用、每个工具怎么用,这就是工具的定义与注册
LangChain 里定义工具有三种常见方式,先准备环境:
pip install langchain langchain-openai python-dotenv pydantic
.env 沿用上一篇阿里 百炼 的配置:
API_KEY=sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
BASE_URL=https://dashscope.aliyuncs.com/compatible-mode/v1
添加三个工具:算食材成本、查门店营业额、查加盟品牌口碑
# 工具定义与注册:演示 LangChain 中三种定义工具的方式,并打印工具元信息
from dotenv import load_dotenv
import os
from langchain.tools import tool
from langchain_core.tools import StructuredTool
from langchain_openai import ChatOpenAI
from pydantic import BaseModel, Field
from typing import Literal
load_dotenv()
# ========== 方式一:@tool 装饰器 + docstring ==========
# 最简单的写法,工具名默认取函数名,描述取 docstring
@tool
def calculate_cost(expression: str) -> str:
"""计算一个餐饮成本表达式,例如 '15*200+3000' 算一天食材成本。"""
try:
result = eval(expression)
return f"算出来了:{expression} = {result} 元"
except Exception as e:
return f"算不了:{e}"
# ========== 方式二:@tool + Pydantic args_schema ==========
# 用 Schema 类约束参数:类型、枚举、默认值、描述都写清楚
class StoreRevenueInput(BaseModel):
store_name: str = Field(description="门店名称,例如 '深圳南山店'")
metric: Literal["daily", "monthly"] = Field(
default="daily",
description="查询周期:daily 日营业额 / monthly 月营业额"
)
@tool(args_schema=StoreRevenueInput)
def get_store_revenue(store_name: str, metric: str = "daily") -> str:
"""查询指定门店的营业额数据。"""
# 模拟查门店数据
amount = 8500 if metric == "daily" else 255000
return f"{store_name} {metric} 营业额 {amount} 元"
# ========== 方式三:StructuredTool.from_function ==========
# 适合把现成函数包装成工具,不用改原函数定义
def check_franchise(brand: str) -> str:
"""查询加盟品牌的口碑和风险情况。"""
return f"{brand}:加盟费 18 万,网上投诉集中在供应链,口碑中等偏下"
franchise_tool = StructuredTool.from_function(
func=check_franchise,
name="check_franchise",
description="查询某个加盟品牌的加盟费、口碑、风险信息"
)
# 1. 打印三个工具的元信息:名字、描述、参数 Schema
tools = [calculate_cost, get_store_revenue, franchise_tool]
for t in tools:
print(f"工具名:{t.name}")
print(f"描述:{t.description}")
print(f"参数 Schema:{t.args_schema.model_json_schema()}")
print("-" * 60)
# 2. 把工具绑定到模型上,模型就知道有这些工具可用了
llm = ChatOpenAI(
model="qwen-plus",
api_key=os.getenv("API_KEY"),
base_url=os.getenv("BASE_URL")
)
llm_with_tools = llm.bind_tools(tools)
# 3. 问一个需要调工具的问题,看模型返回的 tool_calls(调用意图)
response = llm_with_tools.invoke("帮我查一下深圳南山店今天卖了多少钱")
print("模型调用意图:", response.tool_calls)
运行程序,验证输出结果:
1、每个工具注册后都变成了一个带 name、description、args_schema 的标准对象,这是工具的描述。
2、bind_tools 把工具清单传给模型,模型收到问题时会自己判断要不要调工具。
3、tool_calls 是模型返回的调用意图:
[{
'name': 'get_store_revenue', 'args': {
'store_name': '深圳南山店', 'metric': 'daily'}, 'id': 'xxx'}]
这就是上一节说的结构化的调用意图,模型没执行任何本地代码,只是告知程序调用 get_store_revenue,参数是 store_name=深圳南山店, metric=daily。
三、JSON Schema 约束输出
工具调用只是结构化输出的一个场景,另外还有一个常见的场景:让模型把一段自然语言直接转成结构化 数据
大模型输出结构化的数据有什么用呢?example:用大模型做提问相关性校验,当判定问题无关业务场景时触发默认应答,大模型按约束以布尔值 true/false 返回校验结果,后端程序即可直接解析标识、执行对应业务分支
下面是代码案例:勇哥每天收到一堆粉丝私信咨询加盟项目,手动整理太费劲,希望模型把粉丝的描述直接抽成一张评估表:
{
"brand": "甜啦啦", "franchise_fee": 12.0, "payback_months": 8, "risk_level": "高", "location": "长沙"}
描述通常不是一段能直接交给程序解析的文本。这就轮到 JSON Schema 出场了
JSON Schema 的作用是告诉模型:输出里有哪些字段、每个字段是什么类型、哪些值合法。模型按照这份说明书填空,数据就不会跑偏
LangChain 里用 with_structured_output 来控制格式化数据
# 结构化输出:演示用 JSON Schema 约束大模型输出,把自然语言变成可解析的结构化数据
# 主题:勇哥说餐饮——把粉丝发来的加盟项目描述,抽成勇哥能直接判断的结构化评估表
from dotenv import load_dotenv
import os
import json
from langchain_openai import ChatOpenAI
from langchain_core.messages import SystemMessage, HumanMessage
from pydantic import BaseModel, Field
from typing import Literal
load_dotenv()
llm = ChatOpenAI(
model="qwen-plus",
api_key=os.getenv("API_KEY"),
base_url=os.getenv("BASE_URL")
)
# 1. 用 Pydantic 定义输出结构
# 每个字段的 description 会翻译成 JSON Schema 给模型看,模型照着填就不会跑偏
# 这是勇哥评估加盟项目最关心的几个数:投多少钱、多久回本、风险多大
class FranchiseEvaluation(BaseModel):
brand: str = Field(description="加盟品牌名称")
franchise_fee: float = Field(description="加盟费,单位万元,必须是数字不能带单位")
payback_months: int = Field(description="预计回本周期,单位月,必须是数字不能带单位")
risk_level: Literal["低", "中", "高"] = Field(
default="中",
description="风险等级:低 / 中 / 高"
)
location: str = Field(description="计划开店城市")
# 2. 手动把 Schema 塞进 System Prompt + 用 json_mode 保证输出可解析
# 百炼对 with_structured_output 的 json_schema 模式支持不稳,直接用会报 400
# json_mode 只保证输出是合法 json,不保证字段名对,所以要把 Schema 一起喂给模型
schema_json = FranchiseEvaluation.model_json_schema()
system_prompt = (
"你是一个信息抽取助手。请把用户发来的加盟咨询内容,抽取成结构化的 json 格式数据。"
f"必须严格按照下面的 JSON Schema 字段名和类型输出:\n{json.dumps(schema_json, ensure_ascii=False)}\n"
"注意:数值字段只输出数字,不要带单位。"
)
structured_llm = llm.with_structured_output(FranchiseEvaluation, method="json_mode")
# 3. 给一段粉丝发来的自然语言,让模型抽成结构化数据
# 勇哥每天能收到一堆这种私信,手动整理太费劲,让模型自动抽
fan_message = "勇哥,我想加盟'甜啦啦'奶茶,加盟费大概 12 万,品牌方说 8 个月能回本,我打算在长沙开,您觉得风险大不大?"
messages = [
SystemMessage(content=system_prompt),
HumanMessage(content=fan_message)
]
result = structured_llm.invoke(messages)
# 4. result 直接就是个 FranchiseEvaluation 对象,字段、类型都对了
print("解析结果对象:", result)
print(f"品牌:{result.brand}")
print(f"加盟费:{result.franchise_fee} 万")
print(f"回本周期:{result.payback_months} 个月")
print(f"风险等级:{result.risk_level}")
print(f"开店城市:{result.location}")
# 5. 看一下背后的 JSON Schema 长什么样
# 这个 Schema 才是模型真正读到的约束规则
print("\n背后的 JSON Schema:")
print(FranchiseEvaluation.model_json_schema())
运行程序,输出的result 直接就是一个 FranchiseEvaluation 对象
最后打印的 JSON Schema 如下:
{
"properties": {
"brand": {
"description": "加盟品牌名称", "type": "string"},
"franchise_fee": {
"description": "加盟费,单位万元", "type": "number"},
"payback_months": {
"description": "预计回本周期,单位月", "type": "integer"},
"risk_level": {
"default": "中", "description": "风险等级:低 / 中 / 高",
"enum": ["低", "中", "高"], "type": "string"},
"location": {
"description": "计划开店城市", "type": "string"}
}
}
这份 Schema 就是模型真正读到的约束规则。enum 限定枚举值、type 限定类型、description 说明字段含义。结构化输出的本质,就是用 Schema 把模型的输出限制在可控范围内
四、实现最小化的工具调用循环
前面三节都是零件,这一节把它们组装起来,写一个最小的工具调用循环
先回顾下链路:用户输入 → 模型判断要不要调工具 → 返回 tool_calls → 程序执行工具 → 结果回传模型 → 模型生成最终回答
这里的关键是这是个循环:模型调完一个工具,拿到结果后可能还要再调下一个工具,直到它觉得信息够了,不再返回 tool_calls,循环才结束
from dotenv import load_dotenv
import os
from langchain.tools import tool
from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage, ToolMessage
load_dotenv()
# 1. 定义两个工具:成本计算器 + 门店营业额查询
# 勇哥的口头禅:先算账再说话,所以这两个工具是他的标配
@tool
def calculate_cost(expression: str) -> str:
"""计算一个餐饮成本表达式,例如 '8500-15*200' 算日毛利。"""
try:
return f"算出来了:{expression} = {eval(expression)} 元"
except Exception as e:
return f"算不了:{e}"
@tool
def get_store_revenue(store_name: str) -> str:
"""查询指定门店的日营业额。"""
return f"{store_name} 日营业额 8500 元"
tools = [calculate_cost, get_store_revenue]
# 工具名 -> 函数 的映射表,方便后面按名字调用
tool_map = {
t.name: t for t in tools}
# 2. 初始化模型并绑定工具
llm = ChatOpenAI(
model="qwen-plus",
api_key=os.getenv("API_KEY"),
base_url=os.getenv("BASE_URL")
)
llm_with_tools = llm.bind_tools(tools)
# 3. 工具调用循环:核心逻辑就一个 while
# 思路:模型说还要调工具就继续,不调了就结束
def run_agent(user_input: str, max_iter: int = 5) -> str:
"""运行最小工具调用循环,返回最终回答。
Args:
user_input: 用户输入的问题
max_iter: 最大循环次数,防止模型无限调工具
Returns:
模型生成的最终回答文本
"""
messages = [HumanMessage(content=user_input)]
for i in range(max_iter):
# 第一步:把当前消息发给模型,模型决定要不要调工具
response = llm_with_tools.invoke(messages)
messages.append(response)
# 没有 tool_calls,说明模型已经想好最终答案,循环结束
if not response.tool_calls:
print(f"[第 {i+1} 轮] 模型给出最终回答")
return response.content
# 第二步:模型要求调工具,逐个执行
for call in response.tool_calls:
tool_name = call["name"]
tool_args = call["args"]
print(f"[第 {i+1} 轮] 调用工具:{tool_name},参数:{tool_args}")
# 执行工具,拿到结果
result = tool_map[tool_name].invoke(tool_args)
# 第三步:把工具结果以 ToolMessage 喂回消息列表
# ToolMessage 的 tool_call_id 要和模型给的对应上,模型才知道这是哪个工具的返回
messages.append(ToolMessage(
content=str(result),
tool_call_id=call["id"]
))
# 回到循环顶部,带着工具结果再问模型一次
return "达到最大循环次数,强制结束。"
# 4. 跑一个会同时触发两个工具的问题
# 勇哥的粉丝最爱问这种:又想算账又想查数据
if __name__ == "__main__":
answer = run_agent("帮我查一下深圳南山店今天的营业额,再算算扣掉食材成本 1500 后净赚多少")
print("\n最终回答:", answer)
运行一下,会看到这样的输出:
[第 1 轮] 调用工具:get_store_revenue,参数:{'store_name': '深圳南山店'}
[第 1 轮] 调用工具:calculate_cost,参数:{'expression': '8500-1500'}
[第 2 轮] 模型给出最终回答
最终回答: 深圳南山店今天营业额 8500 元,扣掉食材成本 1500 元,净赚 7000 元。
过程如下:
1、第 1 轮模型收到问题,判断要调两个工具(查营业额 + 算成本),返回两条 tool_calls
2、程序逐个执行工具,把结果用 ToolMessage 回传给消息列表——tool_call_id 要和模型给的对应上,模型才知道哪条结果对应哪个调用
3、第 2 轮带着工具结果再问模型,模型这次不再返回 tool_calls,直接给出最终回答,循环结束
数据说明:
max_iter是兜底,防止模型反复调工具陷入死循环。生产环境这个限制必须要有。ToolMessage的tool_call_id用来关联工具调用和返回结果,写错了模型会对应不上。
到这里已经手写了一个能调工具的 Agent 雏形。下一篇 RAG 会给它接上知识库
五、总结
这一篇解决了让模型从只会说到能动手的四个问题:
- Function Calling 原理:模型不执行代码,只返回结构化的调用意图,执行权在程序手里
- 工具定义与注册:三种方式,按场景选,参数 Schema 越清晰模型越不容易传错
- JSON Schema 约束输出:用 Schema 限制模型的输出格式,输出可解析、可控
- 最小工具调用循环:手写循环跑通 User → Model → Tool → Model → User 的完整链路
结构化输出是 Agent 的基础,输出不稳定 Agent 没法正常工作。下一篇进入 RAG,让模型拥有它训练时没见过的知识
六、LuckReport 项目推荐

导航:LuckReport专栏
1、项目简介
Luck-Report 是一款基于开源项目 UReport2 重构的 Java 高性能报表引擎,通过迭代单元格可以实现任意复杂的中国式报表。相较于 UReport2,在技术架构上进行了全新升级,后端基于 SpringBoot 框架开发、前端采用 Vue 框架构建,技术选型贴合当下主流项目开发标准,可精准适配各类实际开发需求。
Luck-Report 提供了全新的基于网页的报表设计器,可以在 Chrome、Firefox、Edge 等各种主流 浏览器 运行(IE 浏览器除外)。使用 Luck-Report,打开浏览器即可完成各种复杂报表的设计制作。
Luck-Report 基于 Apache-2.0 开源协议 开源