news 2026/10/18 5:39:53

大模型工具调用实战:从Function Calling原理到代码实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
大模型工具调用实战:从Function Calling原理到代码实现

1. 为什么大模型需要“外挂工具”:从一个让人抓狂的场景说起

先讲一个我实际遇到的场景。刚接触大模型应用开发那会儿,我用某个开源模型写了个内部小助手,准备让它接管公司周报里的数据整理。模型本身很能聊,你问它“上周销售数据怎么样”,它能说得头头是道。但真让它算一笔账:“A组本周新增客户35个,流失8个,净增多少”,它就露馅了——要么给你编一个“27”,要么说“基于我的分析大约是30左右”。问它今天几号,它给你一个“2025年1月1日”这种鬼话。为什么?因为大模型本质上是一个“文本接龙机器”,它的所有知识都来自训练数据,它没有感知当前时间的能力,也不会真的去执行计算。你让它做算术,它是在“猜一个看起来像算术答案的文本”,而不是“算”。

这就是LLM绑定Tool(工具调用)要解决的核心问题——让大模型从“会说话”变成“会干活”。所谓Tool,就是你暴露给模型的一组函数接口,比如“查数据库”“调用API”“执行Python代码”“发邮件”。模型在理解用户意图之后,决定调用哪个工具、传什么参数,然后把工具返回的真实结果纳回对话里,最终生成让用户满意的答案。这套机制在行业里一般叫Function Calling,也有一些框架叫Tool Calling,本质是同一件事。零基础学AI大模型,很多人第一步会玩对话、写提示词,但真正让大模型进入生产环境发挥价值的,就是这一层“绑定工具”的能力。

我曾经带的A同学第一次听到这个概念时问我:为什么不直接提示词里写“请你计算35减去8”就行了?这个问题的答案恰恰是理解Tool设计的入口——模型的数学能力并不稳定,你无法保证它每次的输出都是可验证的。但如果你给模型挂一个计算器工具,让它把“35, 8, 减法”传给计算器,拿回来的结果就是确定的。工具的意义不只是扩展能力,更是把不可控的模型输出变成可控的系统行为。

这篇文章适合几类人:刚入门大模型想搞懂Tool机制的新手;正在做Agent或RAG项目、需要让模型操作外部系统的人;还有那些已经跑通对话API、但总觉得模型“不够聪明”的开发者。我会用自己的一个完整实战例子,从原理到代码到踩坑,一步步演示LLM怎么绑定、调用工具,把可复用的套路掰开揉碎给你看。整个过程不依赖特定平台,用通用的API风格和示例代码做演示,理解了思路之后,换哪个模型都能搬过去。

2. 搞懂Tool机制的本质:模型不执行任何操作,它只是“发指令”

2.1 工具调用的完整链路:人类世界的一次“代购”

要理解LLM绑Tool,最容易的类比是“代购”。你(用户)想吃一道菜,但你不会做也不会买菜。大模型像一个聪明的管家,它也不会做菜,但它知道附近有菜市场、知道哪家店卖什么、知道怎么下订单。当你说“今晚想吃红烧肉”时,管家不会自己去烧,而是列出一张购物清单:五花肉2斤、冰糖50克、生抽100毫升、八角两颗,然后把清单交给跑腿小哥。跑腿小哥按清单采购,把买回来的菜放到管家面前。最后管家看着这些食材,给你汇报:“菜都买齐了,五花肉多少钱,冰糖多少钱,一共花了多少。”

在这个故事里:

  • 管家就是大模型,它负责理解你的意图、生成工具调用指令;
  • 购物清单就是模型输出的结构化指令(tool_calls),包含工具名称和参数;
  • 跑腿小哥就是你的代码,你负责真正去执行外部函数;
  • 买回来的菜就是函数的返回结果,需要重新“喂”给模型;
  • 管家最后的汇报就是模型结合工具结果生成的最终回复。

记住这个链路,所有Tool调用都逃不出这个模式。很多初学者最大的误区在于:以为给模型注册了函数,模型就会自己去执行代码。不是的,模型只是输出一段结构化的“调用意图”,真正执行函数的是你写的代码。如果你不写执行逻辑,模型只会停留在“我建议你调用get_weather(city='北京')”这一步,不会有任何实际效果。

2.2 ChatCompletion轮到你了:一次完整的REST请求

目前市面上绝大多数大模型的API都兼容OpenAI风格的接口,别管你用的是哪家模型,只要它支持函数调用,请求结构基本都长这样:

{ "model": "some-model-name", "messages": [ {"role": "user", "content": "北京今天适合出门吗?"} ], "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "获取指定城市的当前天气信息", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如北京" } }, "required": ["city"] } } } ], "tool_choice": "auto" }

这个请求里新增了两个核心字段:

  1. tools:你向模型描述的工具列表。每个工具必须有函数名、描述、参数结构。这里的参数结构用JSON Schema描述,模型会根据这个结构来决定“该传什么参数”。
  2. tool_choice:控制模型是否必须调用工具。"auto"表示让模型自己判断该不该调用;"none"表示禁止调用;"required"或指定某个函数名表示强制调用。

如果模型判断确实需要查天气,它返回的消息里会多出tool_calls字段,大致长这样:

{ "role": "assistant", "content": null, "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"city\": \"北京\"}" } } ] }

看到没有,模型并没有去查天气,它只是告诉你:“请帮我调用get_weather,参数是北京”。你拿到这个结果之后,自己在代码里解析函数名和参数,调用真正的get_weather(city="北京")函数,拿到数据之后,再以tool角色的消息发回给API,然后模型才会基于这个结果输出最终文字。整个协议一共就这么几个关键角色:system/user/assistant/tool。不少初学者就是死在“不知道把工具结果用什么角色传回去”,看到返回不了结果就懵了。

2.3 API参数里的关键细节:id、content、role都不能马虎

这里有一个很容易被忽略的细节:当模型返回tool_calls时,那条assistant消息的content通常是null,tool_calls里每条记录有自己的id。你在把工具结果回传给API时,消息格式必须是:

{ "role": "tool", "tool_call_id": "call_abc123", "content": "{\"temperature\": 22, \"condition\": \"晴\"}" }

tool_call_id必须和assistant返回的那条id一一对应,这是把“工具结果关联到哪一次调用”的关键。如果你同时调用了多个工具,模型会返回多条tool_calls,你需要逐条执行、逐条用对应的id回传。顺序方面,建议按模型返回的顺序执行,但严格来说,只要tool_call_id匹配,顺序乱一点也能跑,不过稳定起见别乱。

很多人还会忽略的一点:工具结果content虽然是字符串格式,但你可以放任意内容。放JSON字符串最通用,因为模型的描述能力很强,能给出一段人话。

3. 跑通第一个实战Demo:让模型学会调用两个工具

3.1 环境准备:选什么模型、装什么库

在动手写代码之前,先说说环境。你需要一个支持Function Calling的模型API。现在国内几个主流的大模型平台基本都支持了,包括很多开源模型也能通过兼容接口暴露出来。我这边的做法是:用一套OpenAI风格API的服务地址,配合openai这个Python库来访问。你不需要自己去部署模型,申请一个API Key就行,很多平台都提供免费额度,足够跑通这个Demo。

在终端里执行:

pip install openai

然后用环境变量保存API地址和Key,方便代码里读取。这里有一个建议:不要把API Key硬编码到代码里,哪怕只是个人练习,也养成用环境变量的习惯。等你想把自己的代码发布到GitHub或者给同事看的时候,就会感谢这个习惯。

3.2 定义第一个工具:加法和天气查询

为了演示,我选用两个有代表性的工具:

  1. 加法计算器:展示确定性计算能力,对应那些模型容易算错、但代码一秒能算对的场景;
  2. 天气查询:展示外部数据获取能力,对应需要实时信息的场景。

先设计工具的JSON Schema。用字典存起来,方便后续复用:

def get_weather(city: str) -> str: """模拟查询天气的工具函数,真实项目中可以换成调用天气API""" # 这里用一个固定数据模拟,真实场景应请求外部接口 weather_data = { "北京": {"temperature": 22, "condition": "晴", "wind": "东北风3级"}, "上海": {"temperature": 25, "condition": "多云", "wind": "东南风2级"}, "广州": {"temperature": 28, "condition": "小雨", "wind": "南风4级"}, } if city in weather_data: import json return json.dumps(weather_data[city], ensure_ascii=False) return json.dumps({"error": "暂无该城市数据"}, ensure_ascii=False) def add_numbers(a: float, b: float) -> float: """加法计算器""" return a + b

3.3 构建tools注册列表:格式不对,全盘皆输

这是最容易出错的地方。工具注册列表是严格的JSON结构,你的函数必须转换成标准格式:

tools = [ { "type": "function", "function": { "name": "get_weather", "description": "查询指定城市当前的天气情况,包括温度、天气状况和风力", "parameters": { "type": "object", "properties": { "city": { "type": "string", "description": "城市名称,例如北京、上海、广州" } }, "required": ["city"] } } }, { "type": "function", "function": { "name": "add_numbers", "description": "计算两个数字相加的结果", "parameters": { "type": "object", "properties": { "a": {"type": "number", "description": "第一个加数"}, "b": {"type": "number", "description": "第二个加数"} }, "required": ["a", "b"] } } } ]

这里有几个容易踩的坑:

  • description要写得具体,模型是靠描述来判断“什么时候该用这个工具”的。比如get_weather的描述里如果没有“天气”两个字,模型可能理解偏了。
  • 参数类型要写清楚。"type": "number"和"type": "integer"在JSON Schema中是不同概念,模型遵循得也参差不齐。如果你是整数运算,直接写integer更精确。
  • required数组里列的参数,是模型必须传的。如果你漏了,模型可能返回一个缺参数的调用。

3.4 完成一次完整调用:从用户提问到最终回答

现在,让我们把整个流程串起来。写一个主函数,它负责做四件事:构造第一轮请求、判断模型是否需要调用工具、执行真正的函数、把结果回传给模型。

import json import os from openai import OpenAI client = OpenAI( api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL"), ) def run_conversation(user_input: str): messages = [{"role": "user", "content": user_input}] response = client.chat.completions.create( model="your-model-name", messages=messages, tools=tools, tool_choice="auto", ) assistant_message = response.choices[0].message # 如果模型没有请求调用工具,直接返回文字回答 if not assistant_message.tool_calls: return assistant_message.content # 如果模型请求调用工具,先把assistant的消息加入上下文 messages.append(assistant_message) # 遍历所有工具调用请求 for tool_call in assistant_message.tool_calls: fn_name = tool_call.function.name fn_args = json.loads(tool_call.function.arguments) # 执行真实函数 if fn_name == "get_weather": result = get_weather(city=fn_args.get("city")) elif fn_name == "add_numbers": result = str(add_numbers(a=fn_args.get("a"), b=fn_args.get("b"))) else: result = json.dumps({"error": f"Unknown tool {fn_name}"}, ensure_ascii=False) # 把工具结果加入上下文,用tool角色 messages.append({ "role": "tool", "tool_call_id": tool_call.id, "content": result, }) # 第二次请求,让模型基于工具结果生成最终答案 second_response = client.chat.completions.create( model="your-model-name", messages=messages, tools=tools, tool_choice="auto", ) return second_response.choices[0].message.content if __name__ == "__main__": print(run_conversation("北京现在天气怎么样?")) print(run_conversation("请计算123456789 + 987654321 等于多少?"))

看上去代码不长,但链路里的细节值得逐行拆解:

  1. assistant_message.tool_calls是列表?是的,模型一次可以请求调用多个工具。所以要用for遍历,不能只处理第一个。
  2. 把assistant_message原样加入messages,是为了保留上下文,让模型知道“自己刚才请求了工具”。如果你漏了这一步,第二次请求时模型会失去刚才发出指令的“记忆”,可能答非所问。
  3. 工具函数返回的结果一律先转成字符串。模型接收的content字段就是字符串。你可以返回"22.0",也可以返回JSON字符串,甚至返回一长段文本。

如果你用了国内某些模型平台,API风格基本兼容,但有个别平台对tool_call_id字段的名称略有差异(有的叫tool_call_id,有的叫id),建议先读一下对应API文档。以openai库为准、底层适配,通常问题不大。

3.5 一个容易被忽略的点:要不要把tools放进第二次请求?

答案是要。第二次请求时,我还是把tools和tool_choice="auto"传进去了。为什么?因为模型可能看完第一个工具结果还不够,还想再调用第二个工具。比如用户问“北京和上海哪个更暖和,温差几度”,模型可能先查北京、再查上海,这两次请求就会交叉发生。如果你第二次请求不传tools,模型就丧失了继续调用工具的能力,只能基于已有的部分信息硬答。

那会不会导致无限循环?会,所以实际项目里都会加一个max_iterations限制,比如最多允许模型调用5轮工具。下面我专门用一个章节说这个坑。

4. 调试中最容易翻车的几个地方:我的真实踩坑记录

这一节说几个我实际调试中被折腾到头皮发麻的问题,都是初学者几乎一定会碰到的。

4.1 模型返回的参数,格式别全信

我在某次调试中让模型给add_numbers传两个参数a和b,结果模型返回:

{"a": "一百二十三", "b": 456}

第一个参数居然是中文数字字符串。我json.loads成功了,但把"一百二十三"往函数里传,直接报错。这个问题的根源是:模型在生成arguments时,并不会“真的读”你的JSON Schema去校验,它只是“尽量按格式生成”。所以你的执行层必须做兜底校验,甚至要做类型转换。

我个人现在的做法是,给工具写一个通用的解析函数:

def safe_get_arg(args_dict, key, default=None, cast_type=str): """安全获取参数并做类型转换""" value = args_dict.get(key, default) if value is None: return default try: return cast_type(value) except (ValueError, TypeError): return default

注意,"一百二十三"这种中文数字用float()也转不了,真要处理还得写个中文数字解析器,但那属于极少数情况。实际部署时,你可以选择让模型把参数用number类型描述得更严格,或者在执行层捕获异常并返回错误信息,让模型自己纠正。把错误信息以普通字符串形式返回给模型,它会尝试修正参数重新调用——这是Function Calling里一个非常有用的小技巧,我们后面细说。

4.2 模型返回了不存在的工具名

模型是生成式的,它有可能编造一个你没注册过的工具名。我见过的一次:我只注册了get_weather和add_numbers,模型却返回了一个calculate_discount。为什么?因为它的训练数据里可能见过类似命名的工具,它觉得“这里该调用一个折扣计算函数”。如果我在执行层直接fn_name == "..."对比,那这个调用就会掉进else分支返回Unknown tool错误。

这种问题的处理策略是:执行层遇到未知函数名时,返回一个明确的报错字符串,同时鼓励模型改用其他工具。比如:

result = json.dumps({"error": f"未知工具: {fn_name},可用工具只有 get_weather 和 add_numbers"})

然后模型读了这条错误信息,往往会自己纠正,重新选择正确的工具。这个反馈闭环是整个Function Calling稳定工作的关键。

4.3 循环工具调用,把你的token烧穿

有一次我写的Agent卡死在一个循环里:用户问“上海天气怎么样”,模型调get_weather,天气函数返回{"temperature": 25, "condition": "多云"},但模型看了结果又调了一次get_weather,因为描述里写着“只查北京上海广州”,它可能觉得自己刚才传错了城市,又试了一次。几次反复之后,我的API账单在十分钟内翻了一倍。

解决方案很简单,就是加循环上限:

MAX_ITERATIONS = 5 for _ in range(MAX_ITERATIONS): # ... 请求、判断、执行工具 ... if not assistant_message.tool_calls: break

加上这个上限之后,即便模型想反复调用,超过5轮就直接返回当前的内容,也不会无限烧token。实际项目中,MAX_ITERATIONS根据任务复杂度调节,一般是3到8。如果超过了这个数还没完成,说明要么是任务描述不够清晰,要么是工具本身设计有问题,需要回到Schema层面去修正。

4.4 多工具并行调用:自己管理执行顺序

前面提到,模型一次可能返回多个tool_calls。初学者容易以为模型会“并行执行”它们——不是的,模型只是返回了一个列表,执行是你的代码做的。你对列表可以用顺序执行,也可以用线程池并发执行。如果这些工具之间没有依赖,并发能大幅缩短总耗时;如果有依赖,比如“先查A城市再对比B城市”,那你就要自己理清逻辑。

我在一个项目里写了并发执行版本:

from concurrent.futures import ThreadPoolExecutor def execute_tool(tool_call): fn_name = tool_call.function.name fn_args = json.loads(tool_call.function.arguments) # 根据fn_name分发执行 ... with ThreadPoolExecutor(max_workers=len(tool_calls)) as executor: results = list(executor.map(execute_tool, tool_calls))

注意,用并发时tool_call_id和结果的对应关系千万别搞错。我建议把执行函数写成返回(tool_call.id, result)的元组,再统一组装消息,避免错位。

5. 从Demo到产品:工具设计的边界、成本与控制

5.1 工具粒度怎么拆:粗了模型不会用,细了模型调不过来

工具设计是整个Function Calling应用最耗心思的地方。我见过有人把“查天气”“查湿度”“查风力”“查空气质量”拆成四个工具,结果模型经常不知道用户问“适合跑步吗”该调用哪个;也见过有人把所有数据库查询合并成一个query_database工具,靠一个SQL字符串参数走天下,结果SQL生成的质量惨不忍睹。

经验法则是:以“用户意图”为粒度拆工具。比如对于天气场景,一个get_weather(city, date)就比拆成四个小工具合适。对于数据库场景,如果你只关心查询,一个query_database(sql)就够了;如果还要支持写入、更新、删除,应该拆成独立工具,并且每个工具的描述里写清楚“危险操作,需要二次确认”。工具数量也不是越多越好,控制在5~10个之内,模型的选择准确率会显著提升。超过15个,很多模型的工具选择就开始飘了,你需要在描述上做更严格的约束,或者引入多级Agent来分摊。

5.2 工具执行出错了,正确姿势是“把错误喂回去”

工具执行不可能每次都顺利。比如查天气的外部API超时了,返回了一个网络异常;查数据库时SQL写错了,抛了异常。很多初学者遇到这种情况会直接让整个程序崩溃,这是不对的。正确的思路是:把错误结果转成文本,以tool角色的消息返回给模型,让模型理解发生了什么,并尝试用更合适的参数重新调用。

这里有个关键纪律:错误信息不能裸抛Python异常。模型的上下文不是代码执行环境,它看不懂KeyError: 'city'。你应该先捕获异常,转成描述性的文字,比如“天气服务暂时不可用,请尝试查询其他城市”或“数据库中不存在表user_orders_2025,可用表有users,orders”。模型看到这类信息,才能做出有意义的下一步决策。我实际项目里的做法是给工具套一层统一的包装器:

def tool_wrapper(func): @functools.wraps(func) def wrapped(*args, **kwargs): try: return json.dumps({"status": "success", "data": func(*args, **kwargs)}, ensure_ascii=False) except Exception as e: return json.dumps({"status": "error", "message": str(e)}, ensure_ascii=False) return wrapped

统一格式的好处是:模型看到success或error的标记,一眼就能读懂执行结果。

5.3 成本控制:token消耗远比想象中快

每多一轮工具调用,都要把历史消息重新发给模型,这意味着工具调用越多的对话,token消耗呈线性增长。如果你在系统提示词里塞了一大段背景说明,每一轮都要重复发送,成本会更高。这里有几个我自己常用的省成本策略:

  • 精简系统提示词:不要把长篇的业务规则放在system里,能放进工具描述里的就放工具描述里。
  • 适当截断历史:对于长对话,只需要保留最近几轮的messages,更早的内容该丢就丢。
  • 缓存工具结果:如果用户反复查同一个城市的天气,第二次直接命中缓存,不需要再调API。
  • 只在需要时传tools:如果当前任务完全不涉及工具调用,第二次请求时可以不传tools,能省一部分输入token的解析开销——虽然不算多,但积少成多。

5.4 别迷信“自动判断”:tool_choice该强制时就强制

tool_choice="auto"是默认选项,模型自己判断是否调用工具。但在某些场景下,你应该关闭自动判断,强制固定工具调用。比如你的Agent就是一个“意图分类器”,期望它每次调用classify_intent(text)这个工具,那么tool_choice可以直接指定为:

"tool_choice": {"type": "function", "function": {"name": "classify_intent"}}

这样模型就会“必须”调用该工具,不会自作主张输出文字。对于工作流固定的场景,这个技巧能显著提升稳定性。反过来,如果某个环节不想要模型调用工具,你可以设为"tool_choice": "none",强制纯对话输出。动手实验时建议两套都试试,感受一下模型输出的差异。

6. 深入一步:从工具集合到Agent雏形,这套机制还能怎么玩

6.1 多工具协作的经典案例:查询订单状态

一旦你理解了Tool机制,就会发现它其实是所有Agent应用的基石。这里再拆一个更贴近业务的例子:一个“查订单”的Agent。它需要三个工具:

  1. get_user_info(user_name):根据用户名查到用户ID;
  2. get_orders(user_id, status=None):根据用户ID查订单列表;
  3. get_order_detail(order_id):查订单的详细信息。

当用户问“帮我看看我最近买了什么还没发货”时,模型的调用链路可能是:先调get_user_info拿到用户ID,发现有查过用户再调get_orders,如果订单列表里有未发货的,再继续调get_order_detail。此时你会发现多轮调用是串联的,而且每轮都要把你拿到的中间结果回传给模型,模型才能决定下一步。这其实就是最朴素的Agent雏形——模型作为决策大脑,工具作为手脚,每一轮工具结果为下一轮决策提供依据。

6.2 一个提升稳定性的模式:让工具返回“人能读懂的文本”

有些开发者在工具里返回的是高度结构化的原始数据,比如一个包含几十个键的JSON。模型确实能解析JSON,但并非所有模型都能把嵌套很深的JSON理解准确。我发现一个高阶技巧:许多优秀的Agent系统,工具返回的是经过加工的、语义化的文本。比如查询订单,你的工具函数可以直接返回:

订单202405220012:用户张三,下单日期2024-05-20,包含商品A(2件)、商品B(1件),当前状态为“已发货”,预计送达2024-05-25。

而不是返回一个原始字典让模型去理解。这样虽然牺牲了一点通用性,但模型生成最终答案时会准确得多。对于复杂JSON,可以考虑用一个“摘要工具”先做一层加工,再返回给模型。

6.3 工具调用的安全边界:不该放开的能力绝不放开

最后也是最重要的一点:当你把工具的调用权交给模型时,一定要想清楚这个工具可能被模型“误闯”到什么边界。比如你注册了一个send_email工具,模型可能因为用户一句“帮我给所有人发一封道歉邮件”就把邮件群发了。这不是模型有心干坏事,而是它没有足够的安全意识。

给高风险工具加确认机制,是很成熟的工业界做法。比如在执行send_email之前,先调用ask_user_confirmation工具,让模型先向用户复述“即将给XX发送邮件,内容为XXX,是否确认?”等用户明确同意后再真正执行。严格来说,工具的调用顺序控制也可以写进工具描述里,比如在send_email的description里写明“此工具会真实发送邮件,使用前必须先调用ask_user_confirmation获得用户确认”。模型对这类约束的遵循程度越来越高了,但保险起见,你的执行层仍需要加一道硬校验。

另外一个容易被忽略的安全点是:工具调用过程中传入了用户的原始隐私数据。如果你把用户输入的完整文本一股脑塞给某个外部工具,会产生数据外泄风险。所以API Key、密码、身份证号这类数据,绝不能设计成模型的工具参数。

7. 写在最后的实操心得:API文档翻烂了,不如亲手跑一遍循环

很多人学LLM工具绑定,喜欢先翻几十页文档,再找各种Agent框架去跑现成Demo。我的建议相反:先用最少的代码,把一次完整的“用户提问-模型选工具-代码执行-结果回传-模型总结”跑通,再去看框架。因为这一步跑通之后,你会对消息协议、tool_calls格式、tool_call_id配对这些核心概念建立肌肉记忆,后面无论换哪个框架、哪个模型,都能一眼看懂它的封装在干什么。

实际上,各家对这种机制有各自叫法:有的叫Function Calling,有的叫Tool Use,有的把它并入结构化输出能力。但底层逻辑一致:模型是决策者,不是执行者;你能控制的是工具列表和执行代码,模型负责的是“选择哪个工具、传什么参数、怎么组织最终回答”。搞清楚这个边界,往后你看到任何复杂的Agent流水线,都不会发怵——它们本质上都是这篇文章里那个循环的延伸:多几层工具、多几个条件判断、多几个状态管理而已。

最后再分享一个小技巧。调试工具调用时,把你的messages列表打印出来看。我调试时经常做的一件事是:把第二轮请求发出的完整JSON保存到文件,然后人肉读一遍。你会发现很多“模型为什么这么答”的疑问,在看了完整消息上下文后瞬间就有答案了。我见过不少回传工具结果时把role: tool写错成role: assistant导致一直重试报错的同学,如果早点打日志看一眼,几十秒钟就能定位。调AI项目,日志就是你最好的老师。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/18 5:34:37

Spring Boot健身房管理系统毕设全流程:从数据库设计到答辩指南

一到毕设季,就有不少同学拿着“健身房管理系统”的题目来问我,这个选题在Java毕设里确实常青。Spring Boot做后端、MySQL存数据、页面管会员课程预约收费,业务链路完整,难度又不会高到做不完,是一个性价比很高的经典选…

作者头像 李华
网站建设 2026/10/18 5:33:15

集群安全加固实战:从边界防护到东西向流量管控

给计算机集群加一道安全屏障,这事听起来很宏大,但落到实操上,往往就是一组组规则、一条条策略、几次配置调整。我服务过不少业务集群,从十几台节点的中小规模,到上百台节点的分布式系统都接触过,最大的感受…

作者头像 李华
网站建设 2026/10/18 5:32:18

基于AMESim的NVH仿真:频率域划分、扭振分析与建模实践

简介:面向汽车NVH工程师与仿真分析人员,这份AMESim系统仿真振动噪声NVH解决方案资料,系统讲解如何利用LMS Imagine.Lab AMESim对动力传动、底盘、悬架、制动等系统进行多物理场建模与仿真,覆盖从低频行驶动力学到高频声学舒适性的…

作者头像 李华
网站建设 2026/10/18 5:31:48

JavaWeb学生成绩管理系统:JSP+Servlet+JDBC源码拆解与部署指南

简介:基于JavaWeb的学生成绩管理系统,是一套面向高校计算机相关专业毕业设计以及Java初学者的完整项目源码资源。系统以JSP、Servlet、JDBC为后台框架,配合MySQL数据库,采用B/S结构,包含学生、教师、管理员三类角色&am…

作者头像 李华
网站建设 2026/10/18 5:31:15

大模型编程新利器 Kiro 实测:Specs 与 Hooks 如何吊打 Cursor 工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/18 5:28:56

K8s + AIOps:构建Pod OOM自动定位与自愈体系

前阵子我们这套某在线服务的告警又响了,kubectl top pod看到内存已经摸到容器 limit 边缘,还没等我把第二张监控图点开,Pod 就直接进入了OOMKilled状态。那已经不是第一次了。这种场景做过容器平台运维的朋友应该都不陌生:内存泄漏…

作者头像 李华