【AI】Agent 全栈进阶|工具调用与结构化输出

简介: 文章介绍了大模型的关键能力——Function Calling(函数调用)与结构化输出,主要包含四部分内容: Function Calling 原理,工具定义与注册,JSON Schema 约束输出,最小工具调用循环

200x200

  ◆ 博主名称: QuZhengRong

  AI俘虏,样式苦手

⭐️ Agent专栏Agent

⭐️ LuckReport专栏LuckReport

⭐️ SpringBoot专栏SpringBoot


目录

image

上一篇我们演示了如何调用大模型接口,让大模型回答问题,但从 Agnet 视角出发,它还存在一个很明显的问题:只会说,不会做

你让大模型搜集 数据在电脑上新建一个当日营业额汇总表,它是做不到的,它还缺少和外部交互的"手脚"

要让模型能真正去查数据、跑计算、调接口,还得靠这一篇的主角:Function Calling 与结构化输出

一、Function Calling 原理

Function Calling 是大模型的一项关键受控输出能力,其核心作用是对模型的输出范式进行约束,使其能够将自然语言形式的用户需求,映射为结构化的函数调用意图

数学

What does it mean ?举一个例子:你问大模型深圳南山店今天营业额多少,模型并不清楚具体的营收数据,而程序里刚好有一个 get_store_revenue 函数可以从系统查询营业额,那模型就会返回给你一段这样的结构化数据:

{
   
  "name": "get_store_revenue",
  "arguments": {
   "store_name": "深圳南山店"}
}

程序拿到这段意图后,自己去调对应的函数,再把结果回传给大模型,模型再给出最终回答

模型只判断该不该调工具、调哪个工具、参数填什么,真正的执行权在程序手上。整个链路是这样的:

image

模型返回的这段意图必须是可解析的结构化数据,不能是一堆自然语言,否则程序无法统一处理,所以 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、每个工具注册后都变成了一个带 namedescriptionargs_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 是兜底,防止模型反复调工具陷入死循环。生产环境这个限制必须要有。
  • ToolMessagetool_call_id 用来关联工具调用和返回结果,写错了模型会对应不上。

到这里已经手写了一个能调工具的 Agent 雏形。下一篇 RAG 会给它接上知识库

五、总结

这一篇解决了让模型从只会说到能动手的四个问题:

  1. Function Calling 原理:模型不执行代码,只返回结构化的调用意图,执行权在程序手里
  2. 工具定义与注册:三种方式,按场景选,参数 Schema 越清晰模型越不容易传错
  3. JSON Schema 约束输出:用 Schema 限制模型的输出格式,输出可解析、可控
  4. 最小工具调用循环:手写循环跑通 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 开源协议 开源

2、在线体验

相关文章
人工智能 JavaScript 测试技术
212 4
|
1月前
|
机器学习/深度学习 缓存 人工智能
SSE流式传输稳定性进阶:心跳保活、断连重连、分片处理与双端容错实战.162
SSE(Server-Sent Events)是基于HTTP的单向流式协议,天然适配大模型逐字输出场景。具备轻量、兼容性好、自动重连、低内存占用等优势,相比WebSocket更契合服务端单向推送需求,是AI应用流式响应的理想选择。
367 7
人工智能 安全 网络安全
23 0
Web App开发 人工智能 Java
48 2
人工智能 Java BI
71 3
存储 运维 监控
23 1
人工智能 Cloud Native 算法
123 0
|
2月前
|
消息中间件 监控 Java
阿里云云消息队列RabbitMQ版配置流程:从实例创建到消息收发全解析
本文系统梳理了阿里云云消息队列RabbitMQ版的完整配置流程,涵盖服务开通、实例创建、Vhost隔离、Exchange与Queue配置、用户权限管理、Python与Java代码接入实践、监控告警设置及常见问题排查。文章深入解析了Exchange的四种类型路由规则、开源身份验证与RAM两种权限模式、消息堆积的根源分析与QoS参数调优策略。提供可复用的代码示例与实用的运维建议,帮助开发者从零开始快速搭建稳定可靠的生产级消息队列系统。
|
11天前
|
设计模式 Web App开发 人工智能
【AI】Agent 全栈进阶|系统化学习路线专题
描述 Agent 的概念、核心构成,规划出一套循序渐进的 Agent 开发学习路径,从大模型调用、工具调用、RAG、运行模式、记忆机制再到工程化调试,同时附上多款适合入门钻研的开源参考项目
553 4
|
2月前
|
人工智能 运维 安全
生成式 AI 赋能下大规模网络钓鱼攻击与防御技术研究
本文以2026年谷歌起诉Outsider Enterprise滥用Gemini AI实施大规模钓鱼攻击为案例,剖析AI赋能钓鱼攻击的产业化、高仿真、多渠道新特征;揭示传统防护短板,提出基于语义分析、行为研判的四层动态检测方案,并配套可运行Python代码;倡导构建“技术+渠道+宣教+法律”全域闭环防御体系。(239字)
222 4

热门文章

最新文章