尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

【Agent开发第五期】Tool Use 工具调用,给 Agent 装上“手“

【Agent开发第五期】Tool Use 工具调用,给 Agent 装上“手“ 文章目录一、前言二、概念对齐:Agent 和普通 LLM 的核心区别2.1 回顾:Alex 只会动嘴2.2 工具调用:给模型装手2.3 工具循环:不是一问一答2.4 本期组装公式三、动手做:给 Alex 装上手3.1 用 JSON schema 描述工具3.2 工具实现:真实执行3.3 工具循环:本期核心3.4 tool_choice 参数3.5 messages 角色演进四、跑起来:工具调用的实际体验4.1 自动演示:问时间4.2 问计算:不再心算出错4.3 纯聊天:模型自己判断不调工具4.4 安全性:为什么 calculate 用 AST 而不是 eval4.5 五期演进五、执行脚本六、总结一、前言第 04 期 Alex 已经有了记忆和人设,但它还是只能动嘴——你问现在几点,它会老老实实承认我没有实时信息获取能力;你问17 乘 23 等于多少,它可能会心算出错,给你一个近似值。这是第 01 期就埋下的伏笔:模型的知识停留在训练数据里,它无法访问外部世界——不能看时间、不能查数据库、不能调接口。LLM 是一个博学但无手脚的顾问,它什么都能聊,但什么都做不了。这一期我们给 Alex 装上手:用 JSON schema 描述工具,让模型自己决定何时调用、传什么参数,执行后把结果喂回去,最终生成准确回答。看完你就能:用tools参数 JSON schema 给模型描述工具实现工具循环:模型决定调用 → 执行 → 结果喂回去 → 生成最终回答理解role: tool消息和tool_choice参数用 AST 安全执行数学表达式(防止 eval 注入)本文是 Agent 教学系列第 05 期的实战笔记,干货为主,各位看官将就着看。二、概念对齐:Agent 和普通 LLM 的核心区别2.1 回顾:Alex 只会动嘴前 04 期我们做了三件事:期做了什么Agent 能力01-02API 调用 循环能说话,但无记忆03messages 回灌有短期记忆04system prompt有身份(Alex 技术助理)但 Alex 还是一个只会说的顾问——问时间它说我不知道,问算术它心算可能出错。这就是 Agent 和普通 LLM 的核心区别:Agent 能动手,不只是动嘴。2.2 工具调用:给模型装手OpenAI SDK 的 chat completions 接口支持tools参数——你用 JSON schema 描述工具,模型据此决定何时调用、传什么参数。TOOLS[{type:function,function:{name:get_current_time,description:获取当前日期和时间。当用户问现在几点时调用。,parameters:{type:object,properties:{timezone:{type:string,description:目标时区,如 Asia/Shanghai,}},required:[],},},},]模型看到的是description和parameters的 JSON schema。它据此判断这个问题我需要调工具还是我自己就能答。2.3 工具循环:不是一问一答加了工具后,对话不再是一问一答的单次调用,而是一个循环:用户: 现在几点了? ↓ 第 1 次调用 API (toolsTOOLS, tool_choiceauto) → 模型返回: tool_calls[get_current_time()] → 模型没有文本回答,只说我要调工具 ↓ 执行工具: datetime.now() → 结果: {datetime: 2026-08-14 15:30:00, weekday: 周四} ↓ 把结果喂回去: messages.append({role: tool, content: result}) ↓ 第 2 次调用 API (带着工具结果) → 模型返回文本: 现在是 2026年8月14日 15:30,周四。这就是 Agent 的本质:模型决定调什么工具,代码真正执行,结果喂回去让模型生成最终回答。2.4 本期组装公式有工具的 Agent step04(system prompt history) 工具定义 工具循环累积式:step05 step04 Tool Use。不重构前期代码,只新增一层。三、动手做:给 Alex 装上手3.1 用 JSON schema 描述工具本期装了 2 个工具:get_current_time(拿时间)和calculate(四则运算)。TOOLS[{type:function,function:{name:get_current_time,description:获取当前日期和时间。当用户问现在几点今天星期几时调用。,parameters:{type:object,properties:{timezone:{type:string,description:目标时区,如 Asia/Shanghai。默认 Asia/Shanghai。,}},required:[],},},},{type:function,function:{name:calculate,description:进行四则运算。当用户要求计算数学表达式时调用。,parameters:{type:object,properties:{expression:{type:string,description:数学表达式,如 17 * 23,}},required:[expression],},},},]关键是description——模型靠它判断该不该调这个工具。写清楚,模型才能对上号。3.2 工具实现:真实执行execute_tool()是工具的手——模型决定调什么,这里真正执行:defexecute_tool(name:str,arguments:dict)-str:ifnameget_current_time:nowdatetime.now()weekdays[周一,周二,周三,周四,周五,周六,周日]returnjson.dumps({datetime:now.strftime(%Y-%m-%d %H:%M:%S),weekday:weekdays[now.weekday()],},ensure_asciiFalse)ifnamecalculate:exprarguments.get(expression,)treeast.parse(expr,modeeval)result_safe_eval(tree.body)returnjson.dumps({result:result},ensure_asciiFalse)真实场景里这里可能是 HTTP 请求(查天气、查数据库、发邮件)——结构完全一样。3.3 工具循环:本期核心把第 04 期的chat()升级为chat_with_tools():defchat_with_tools(messages:list[dict])-tuple[str,list[dict],int,int]:whileTrue:responseclient.chat.completions.create(modelMODEL,messagesmessages,toolsTOOLS,# ← 告诉模型有哪些工具tool_choiceauto,# ← 模型自己决定要不要调max_tokens1000,)msgresponse.choices[0].message# 情况 A:模型决定调用工具ifmsg.tool_calls:messages.append({role:assistant,content:msg.contentor,tool_calls:[...]})fortool_callinmsg.tool_calls:resultexecute_tool(fn_name,fn_args)messages.append({role:tool,tool_call_id:tool_call.id,content:result,})continue# 继续循环,让模型生成最终回答# 情况 B:模型返回文本回答(没有调用工具)returnmsg.content,messages,total_prompt,total_completion核心逻辑:模型返回tool_calls时,执行工具、把结果喂回去、继续循环;模型返回文本时,循环结束。3.4 tool_choice 参数tool_choiceauto# 模型自己决定要不要调工具(默认,最常用)tool_choicenone# 禁止调工具(纯粹聊天时用,省 token)tool_choicerequired# 强制调工具(必须动手,不能只动嘴)本期用auto——模型判断:问时间就调工具,问你好就不调。3.5 messages 角色演进到第 05 期,messages 结构变成:第 04 期: [system, user, assistant, user, assistant, ...] 第 05 期: [system, user, assistanttool_calls, tool, assistant, user, ...]多出来的role: tool消息携带工具执行结果,会被纳入历史回灌——模型后续调用也能看到之前调过什么工具、结果是什么。四、跑起来:工具调用的实际体验4.1 自动演示:问时间程序启动会自动问现在几点了?今天星期几?,打印工具循环全过程:[工具调用] get_current_time({}) [工具结果] {datetime: 2026-08-14 15:30:00, weekday: 周四} [最终回答] 现在是 2026年8月14日 15:30,周四。模型没有猜时间,而是调用了真正的工具——这就是 Agent 和普通 LLM 的核心区别。4.2 问计算:不再心算出错输入帮我算一下 17 * 23:[工具调用] calculate({expression: 17 * 23}) [工具结果] {expression: 17 * 23, result: 391} [最终回答] 17 * 23 391LLM 不擅长精确计算,有了工具就不再出错。4.3 纯聊天:模型自己判断不调工具输入你好或Python 怎么读文件,模型不调工具直接回答——tool_choiceauto让模型自己判断。4.4 安全性:为什么 calculate 用 AST 而不是 eval# 危险!模型如果传入恶意表达式,eval 会执行任意代码resulteval(expression)# __import__(os).system(rm -rf /)# 安全:用 AST 解析,只允许四则运算节点treeast.parse(expr,modeeval)result_safe_eval(tree.body)# 只处理 Num / BinOp / UnaryOp工具执行是 Agent 安全的关键——模型传来的参数不可信,必须当作用户输入来校验。execute_tool()是代码边界,任何参数都要做输入校验。4.5 五期演进期messages 结构Agent 能力01[user]单次调用02[user](每次独立)循环对话(无记忆)03[user, assistant, ...]有短期记忆04[system, user, assistant, ...]有记忆 有身份05[system, user, assistanttool_calls, tool, assistant, ...]有记忆 有身份 有工具前 05 期:模型从只会说到能记住到有身份再到有工具。Agent LLM 记忆 人设 工具,四个零件齐了。五、执行脚本以下是第 05 期的完整代码,累积式,可直接运行:#!/usr/bin/env python3step05_tool_use.py — 第 05 期:Tool Use 工具调用 本期目标: 1. 给 Alex 装第一个工具 get_current_time,解决第 01 期不知道几点的伏笔 2. 实现工具循环:模型决定调用工具 → 执行 → 把结果喂回去 → 生成最终回答 3. 理解 JSON schema 描述工具、tool 角色消息、tool_choice 参数 累积式:step05 step04 工具定义 工具循环 (保留 step04 的 system prompt 人设 history 记忆,只新增工具层) 运行: python code/step05_tool_use.py importosimportjsonimportastimportoperatorfromdatetimeimportdatetimefrompathlibimportPathfromdotenvimportload_dotenvfromopenaiimportOpenAIfromprompt_toolkitimportpromptfromprompt_toolkit.historyimportInMemoryHistory load_dotenv()clientOpenAI(api_keyos.environ[DEEPSEEK_API_KEY],base_urlos.environ.get(DEEPSEEK_BASE_URL,https://api.deepseek.com),)MODELos.environ.get(DEEPSEEK_MODEL,deepseek-chat)MAX_ROUNDS10SOUL_PATHPath(__file__).parent.parent/templates/SOUL.mddefload_system_prompt()-str:ifnotSOUL_PATH.exists():raiseFileNotFoundError(f找不到人设文件:{SOUL_PATH})returnSOUL_PATH.read_text(encodingutf-8)# 工具定义 TOOLS[{type:function,function:{name:get_current_time,description:获取当前日期和时间。当用户问现在几点今天星期几时调用。,parameters:{type:object,properties:{timezone:{type:string,description:目标时区}},required:[],},},},{type:function,function:{name:calculate,description:进行四则运算。当用户要求计算数学表达式时调用。,parameters:{type:object,properties:{expression:{type:string,description:数学表达式}},required:[expression],},},},]# 工具实现 _ALLOWED_BINOPS{ast.Add:operator.add,ast.Sub:operator.sub,ast.Mult:operator.mul,ast.Div:operator.truediv,ast.Mod:operator.mod,ast.Pow:operator.pow,}_ALLOWED_UNARYOPS{ast.UAdd:operator.pos,ast.USub:operator.neg}def_safe_eval(node):ifisinstance(node,ast.Constant):ifisinstance(node.value,(int,float)):returnnode.valueraiseValueError(f不支持的常量:{node.value!r})ifisinstance(node,ast.BinOp):op_ALLOWED_BINOPS.get(type(node.op))ifopisNone:raiseValueError(f不支持的运算符:{type(node.op).__name__})returnop(_safe_eval(node.left),_safe_eval(node.right))ifisinstance(node,ast.UnaryOp):op_ALLOWED_UNARYOPS.get(type(node.op))ifopisNone:raiseValueError(f不支持的运算符:{type(node.op).__name__})returnop(_safe_eval(node.operand))raiseValueError(f不支持的表达式类型:{type(node).__name__})defexecute_tool(name:str,arguments:dict)-str:ifnameget_current_time:nowdatetime.now()weekdays[周一,周二,周三,周四,周五,周六,周日]returnjson.dumps({datetime:now.strftime(%Y-%m-%d %H:%M:%S),weekday:weekdays[now.weekday()],},ensure_asciiFalse)ifnamecalculate:exprarguments.get(expression,)try:treeast.parse(expr,modeeval)result_safe_eval(tree.body)returnjson.dumps({result:result},ensure_asciiFalse)exceptExceptionase:returnjson.dumps({error:f计算失败:{e}},ensure_asciiFalse)returnjson.dumps({error:f未知工具:{name}},ensure_asciiFalse)# 工具循环 defchat_with_tools(messages):total_prompt0total_completion0whileTrue:responseclient.chat.completions.create(modelMODEL,messagesmessages,toolsTOOLS,tool_choiceauto,max_tokens1000,)msgresponse.choices[0].message total_promptresponse.usage.prompt_tokens total_completionresponse.usage.completion_tokensifmsg.tool_calls:messages.append({role:assistant,content:msg.contentor,tool_calls:[{id:tc.id,type:function,function:{name:tc.function.name,arguments:tc.function.arguments}}fortcinmsg.tool_calls],})fortool_callinmsg.tool_calls:fn_nametool_call.function.name fn_argsjson.loads(tool_call.function.arguments)resultexecute_tool(fn_name,fn_args)messages.append({role:tool,tool_call_id:tool_call.id,content:result,})continue# 把最终回答也加入历史,history 能看到完整对话messages.append({role:assistant,content:msg.content})returnmsg.content,messages,total_prompt,total_completion# ... (main 函数与第 04 期类似,新增 tools 命令、history 查看历史、工具循环)完整代码见仓库code/step05_tool_use.py,单文件可直接运行。六、总结一句话回顾:Agent LLM 记忆 人设 工具。前 04 期搞定了前三个,本期用 JSON schema 描述工具、用工具循环让模型动手——不再靠猜,而是调用真实工具获取准确信息。三个关键概念记牢:JSON Schema 描述工具→ 告诉模型:有哪些工具、叫什么、做什么、需要什么参数工具循环→ 模型决定调用 → 执行 → 结果喂回去 → 生成最终回答(可能多次调用)tool 角色消息→role: tool携带工具结果,被纳入历史回灌一行代码记住本期:responseclient.chat.completions.create(modelMODEL,messagesmessages,toolsTOOLS,tool_choiceauto,# ← 告诉模型有哪些工具,让它自己决定)模型看到 tools 的 JSON schema,决定调工具还是自己答。调了就执行,结果喂回去再调一次——这就是 Agent 的动手能力。安全性提醒:工具执行是代码边界,模型传来的参数不可信!calculate用 AST 而不是 eval,就是防止注入攻击。任何工具的输入参数都要做校验。适用场景:这篇适合已经给 Agent 加了人设、想让 Agent 能调用外部工具的人。下一期预告:第 06 期——Skills 按需加载。Alex 现在有 2 个工具,但如果需要 20 个工具呢?全部塞进 TOOLS 列表会占大量 token。下一期用skills/目录 SKILL.md frontmatter 实现按需加载——只加载当前任务需要的技能。感谢各位看官的一路陪伴,大家都再接再厉!
返回列表