工具调用实战:给Agent装上手脚,从对话AI到能执行命令的编程助手

发布时间:2026/10/22 7:58:36
工具调用实战:给Agent装上手脚,从对话AI到能执行命令的编程助手
写代码写得越多越会发现一个道理Agent 光能聊天是没有灵魂的。第1天我们把大模型接进了终端做出了一个能对话、能流式输出、能记住上下文的“聊天机器人”但它本质上只是个高级点的对话框——你问它问题它回答你让它改个文件它说“好的你可以打开 某文件 修一下”然后就没有然后了。这正是我启动“30天复刻 Claude Code”这个项目的第2天要解决的核心问题给 Agent 装上手脚让它能真正干活。这天的任务很明确——实现工具调用Tool Calling。我给自己定的验收标准就一句话在一行提示词里告诉 Agent“去读一下 项目配置 文件看看端口配置然后用测试命令启动服务”它能自己完成读文件、执行命令、分析结果、给出结论全程不用我手动介入。这篇文章就完整记录我第2天踩过的坑、写下的代码、还有改了三版才理顺的 Agent 执行循环。如果你想自己从零搭一个终端 AI 助手或者想搞明白工具调用到底是怎么一回事这一篇应该能帮你省掉不少弯路。1. 为什么第2天就急着加工具先说说我为什么把“工具调用”排在第2天。很多人做 AI 应用习惯先把对话体验打磨到位再考虑工具。但做过 Agent 的人都知道工具调用不是一个“增强功能”它是 Agent 与聊天机器人的分水岭。1.1 从“会说”到“会做”Agent 的核心闭环我第1天做出来的是一个标准的对话程序用户输入 → 调用大模型 → 打印回复。这个流程可以叫“AI 聊天”但配不上“Agent”这个词。Agent 的含义是能够自主行动的智能体它至少要具备三个环节感知从用户指令和环境中获取信息比如读取文件、查看当前目录决策基于信息判断下一步该做什么比如决定先跑测试还是先改代码行动执行具体的操作并观察结果比如执行命令、编辑代码这三个环节构成一个循环。而没有工具调用的纯聊天程序永远只能停留在“说一说”的阶段它无法感知环境也无法行动。这也是为什么 Claude Code 这类产品看起来像是“有手有脚”的程序员而普通的 AI 聊天框只是“纸上谈兵”的顾问。1.2 工具调用解决的真正痛点你有没有遇到过这种情况让 AI 帮你分析项目它会问你“请把 项目配置 文件内容发给我”。来来回回十几轮效率极低。工具调用的本质就是让模型自己具备“取信息”的能力。大模型本身是个“没有感官的脑子”它训练时见过大量代码但它看不见你磁盘上的文件也不知道你当前目录里有什么。工具调用相当于给这个脑子接上眼睛读文件、手编辑文件、以及一套行动装置执行命令。模型在对话中判断“我需要看某个文件的内容”然后不是直接输出一段话而是输出一个结构化的“工具调用请求”你的程序捕获这个请求执行真实的文件读取再把结果返回给模型。模型基于真实内容继续推理。注意很多人以为工具调用是模型主动“学会了”调函数其实不是。它是你预先声明了一系列工具的描述模型在生成时根据对话内容自行选择是否发起调用。真正执行工具的是你的代码模型只负责“提出请求”。1.3 为什么用两天时间才做到这一步我看过不少类似的教程上来就贴一段 ReAct 循环的代码然后说“看Agent 做完了”。但自己动手写才发现从“能聊”到“能干”的跨度比想象中大得多。你至少需要搞定四个层次的问题API 层的工具描述格式模型怎么知道你有什么工具执行层的实现工具真的去读写文件、执行命令要处理多少边界情况循环层的控制模型连续调用工具时循环什么时候终止安全层如果 Agent 执行了一条危险命令谁来兜底第2天的时间我大部分花在上述第1、3、4层上第2层工具本身的实现反而是最好写的。这篇文章就按这个脉络来讲。2. 工具定义与 Schema 设计模型凭什么正确调用工具调用的第一件事不是写工具函数而是写“工具说明书”。你把工具说明喂给模型模型才知道在什么场景下该调用哪个工具参数应该怎么填。这一步很像给一个新来的实习生写任务交接文档——文档写得越清楚实习生干活越靠谱。2.1 一次工具调用的完整对话周期要理解工具定义的作用得先看清大模型 API 的工作方式。目前主流大模型厂商提供的 tool/function calling 能力本质上是在标准对话之外增加了一种新的消息类型。当你在请求里声明了可用工具后模型可能在回复中返回一个 tool_calls 字段里面包含工具名和 JSON 格式的参数。整个流程分两轮走第一轮请求你把用户的问题、历史对话、工具列表一起发给模型。模型的回复里可能带着一个或多个“工具调用请求”不再是一段普通文本。你的程序执行这些工具拿到真实结果。第二轮请求你把工具执行结果作为一条新消息追加到对话里再次发给模型。模型看完结果给出最终回复或继续发起新的工具调用。这样设计的精妙之处在于工具调用被还原成了一种“对话内容”。模型不需要真的具备执行能力它只需要在正确的时机“说出”自己想用什么工具、传什么参数你的程序负责把这些“想法”变成现实。整个过程是标准的多轮对话只是中间插入了几条 role 为 tool 的消息。2.2 工具描述写不好模型就乱调用我第一天写工具定义时犯了一个典型错误把工具描述写得非常简短。比如执行命令的工具我写的是“Execute a command”参数只写了“command: string”。结果模型经常在只需要读文件的时候就去执行命令或者在需要执行测试时用了一个过时参数。后来我改成详细描述效果立刻改善。一个合格的工具描述至少包含三部分工具概述一句话说明这个工具解决什么问题、通常在什么场景下使用参数说明每个参数的含义、类型、取值范围、是否必填最好带上示例使用约束什么情况下不建议使用这个工具避免模型误调用下表是我第2天最终定稿的三个核心工具定义可以直接拿去参考工具名一句话描述核心参数使用约束exec_command在项目目录下执行终端命令适用于运行测试、编译、查看目录等command(必填)要执行的命令字符串timeout(选填)超时秒数不适用于交互式命令修改生产环境前需人工确认read_file读取指定文本文件的全部或部分内容path(必填)文件路径offset(选填)起始行号limit(选填)读取行数仅限项目工作目录内的文件edit_file基于行号替换或插入代码片段path(必填)目标文件start_line(必填)起始行end_line(选填)结束行replacement(必填)替换内容推荐先 read_file 拿到最新文件结构再编辑2.3 关键参数设计的三点经验参数设计的细节直接决定工具调用的准确率我实测下来有三个坑都是血泪教训第一个坑参数别贪多。我给 exec_command 设计了8个参数包括 shell、env、cwd、timeout、encoding 等。结果模型经常漏传关键参数或者把互斥参数一起传。后来精简到2个参数调用准确率明显提升。Agent 工具的参数设计原则应该是“能少则少能省则省”。第二个坑不要过度依赖 default 值。刚开始我给每个参数都加了默认值想着这样模型省事。但实测发现一旦某个参数有默认值模型就可能忽略你希望它显式指定的值。比如我给 read_file 的 limit 默认设为100模型想读完整文件时就不会主动把 limit 调大读取不完整导致后续编辑出错。后来我把 limit 设为可选但没有默认值要求模型根据文件大小自行决定。第三个坑description 里要写“反面案例”。模型经常在拿不准的时候乱调用工具。比如它应该直接凭知识回答的问题非要去执行一条无关命令。我在描述里加了这些约束“仅当需要获取运行时信息或验证代码行为时使用”“不应使用此工具查询通用编程知识”调用次数立刻降了下来。大模型其实很擅长读说明书只是你得把“什么不该做”也写清楚。3. 实现 Agent 循环从一次调用到完整执行闭环工具定义写完接下来就是把这套逻辑跑起来。我在第2天花了最多时间的地方不是工具函数本身而是 Agent 的“主循环”。这个循环决定了 Agent 是像一个有脑子的助手还是像一个只会执行单条指令的机器人。3.1 Agent 主循环的完整代码骨架我最终实现的 Agent 循环用 Python 写核心思路是只要模型的返回里还有 tool_calls就持续执行并回传结果直到模型输出纯文本回复。代码骨架如下import json from openai import OpenAI client OpenAI() MAX_ITERATIONS 8 # 防止模型无限循环调工具 def run_agent(user_message: str): messages [{role: user, content: user_message}] for _ in range(MAX_ITERATIONS): response client.chat.completions.create( modelyour-model-name, messagesmessages, toolsTOOL_DEFINITIONS, # 第2节定义的工具列表 tool_choiceauto ) message response.choices[0].message # 情况1模型没有发起工具调用对话结束 if not message.tool_calls: print(最终回复:, message.content) return message.content # 情况2模型发起了一个或多个工具调用 # 先把模型的消息追加到历史里保证上下文连贯 messages.append(message) for tool_call in message.tool_calls: print(f[Agent] 调用工具: {tool_call.function.name}) print(f 参数: {tool_call.function.arguments}) result execute_tool(tool_call.function.name, json.loads(tool_call.function.arguments)) # 工具执行结果一定要绑定对应的 tool_call_id messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) # 超出最大迭代次数强制收尾 return 已达到最大工具调用次数任务可能未完成建议拆分后重试。这套循环看着简单但里面有几个细节直接决定成败。我逐个讲。3.2 为什么模型消息必须追加进历史刚开始我犯过一个错误只在第一轮请求里带用户消息工具执行完直接把结果发过去没有带上模型那条 tool_calls 消息。结果模型经常表现得很困惑——它明明请求调用工具了但下一轮看到工具结果时却“不知道”自己之前要干嘛了。原因在于工具调用是一个多轮对话过程模型发起调用请求的那条消息本身就是对话上下文的一部分。你必须把包含 tool_calls 的 assistant 消息完整追加到 messages 数组里后面跟一条或多条 tool 消息这样模型才能把“我之前的请求”和“工具返回的结果”关联起来。这个模式的完整消息序列长这样[ {role: user, content: 帮我看看 项目配置 的端口配置然后启动项目}, {role: assistant, content: null, tool_calls: [{id: call_123, function: {name: read_file, arguments: {\path\: \项目配置\}}}]}, {role: tool, tool_call_id: call_123, content: port8080\n...} ]3.3 工具结果回传id 匹配是硬规矩另一个我踩过的坑是 tool 消息的 id 匹配。tool 消息必须包含 tool_call_id 字段并且这个 id 一定要和 assistant 消息里那条 tool_call 的 id 完全一致。如果漏写或写错大模型 API 会直接报错或者更糟——模型完全忽略工具结果自顾自地回复。这个 id 是模型生成的随机字符串不是你自己编的。执行工具时记录下每条 tool_call.id回传时原样带上就行。我一开始图省事想着用数字序号代替结果 API 报 400。后来老老实实用模型返回的 id问题秒消。3.4 循环次数限制的取舍MAX_ITERATIONS 这个参数是第2天让我纠结最久的地方。设置小了复杂的多步任务跑不完Agent 中途“罢工”设置大了遇到模型陷入死循环时比如它反复调用 read_file 读同一份文件会白白消耗大量 token。我最终定的是8次。实测大部分任务在3~5次工具调用内完成8次足够覆盖大多数场景又能在失控时及时止损。另外我还加了一个小优化在每轮循环开始时统计一下 messages 里的 token 总量如果超过模型上下文窗口的 80%就主动截断早期的对话历史。这个优化在后面缓存和上下文管理时要重点展开第2天先做个兜底就够用。3.5 工具执行的并发与顺序大模型 API 允许在一次回复里返回多个 tool_calls也就是模型认为需要同时做几件事。我的处理策略是顺序执行一个工具跑完拿到结果再跑下一个。这样做的好处是结果顺序清晰每一轮循环都能立刻看到每个工具的输出。虽然模型本来可以并发调用多个工具但在没有一个完善并发框架的情况下顺序执行更可控也不容易出错。给整个工具执行环节包一个总超时比如60秒避免某个命令长时间挂起拖死整个循环。这个细节我在第4节还会讲到。4. 三个核心工具的实现给 Agent 装上眼睛和手工具循环跑通之后剩下的事情就是把工具本身写好。第2天我只实现了三个工具执行命令、读取文件、编辑文件。这三个工具是 Claude Code 这类产品最基础的能力后面所有复杂功能代码搜索、测试执行、仓库操作都在这三个工具之上扩展。4.1 exec_command让 Agent 拥有“手”这个工具真的去执行终端命令是 Agent 能“干活”的关键。我用 Python 的 subprocess 模块实现import subprocess, os, time def exec_command(command: str, timeout: int 30): start time.time() try: proc subprocess.run( command, shellTrue, # 允许管道、重定向等复杂命令 capture_outputTrue, textTrue, timeouttimeout, cwdPROJECT_ROOT, # 锁定工作目录 env{**os.environ, PYTHONUNBUFFERED: 1} ) stdout proc.stdout[:20000] # 强制截断防止上下文爆炸 stderr proc.stderr[:5000] code proc.returncode return json.dumps({exit_code: code, stdout: stdout, stderr: stderr}) except subprocess.TimeoutExpired: return json.dumps({exit_code: -1, stdout: , stderr: f[Timeout] 命令执行超过 {timeout} 秒已终止})这里最容易被初学者忽略的是 output 截断。如果工具输出几万行日志全部回传给模型轻则 token 暴涨、成本翻倍重则直接超限报错。我设了 stdout 2万字符的上限并在截断处加提示“[输出已截断]”让 Agent 知道信息不全需要的话可以换一种更精准的方式继续查询比如 grep。shellTrue 这个参数同样值得讨论。直接用 shell 执行命令好处是管道、重定向、通配符都能用坏处是命令注入风险。我的折中方案是命令行本身来自模型生成的 JSON 参数在进入 subprocess 前先做一次危险命令检查见第5节同时把工作目录锁定在项目根目录。单用户本地工具场景下这个方案够用但如果要部署成多用户服务得改成参数数组模式并且做严格的命令白名单。注意不要在 timeout 问题上心软。模型有时候会生成一个长时间挂起的命令比如启动一个不会退出的开发服务器如果没有 timeout你的整个 Agent 循环会卡死在工具执行这一步且手动中断一次可能还会引入别的麻烦。4.2 read_file给 Agent 装上“眼睛”执行命令能拿到运行信息但代码修改必须先读懂文件。read_file 的实现核心是行号前缀def read_file(path: str, offset: int 1, limit: int 200): abs_path os.path.abspath(os.path.join(PROJECT_ROOT, path)) # 安全校验不允许越出项目根目录 if not abs_path.startswith(PROJECT_ROOT): return json.dumps({error: 路径越界仅允许访问项目目录内的文件}) if not os.path.exists(abs_path): return json.dumps({error: f文件不存在: {path}}) with open(abs_path, r, encodingutf-8, errorsreplace) as f: lines f.readlines() total len(lines) end min(offset limit - 1, total) selected lines[offset-1:end] # 拼接行号方便后续 edit_file 精确定位 numbered [f{ioffset}\t{line.rstrip()} for i, line in enumerate(selected)] content \n.join(numbered) return json.dumps({ path: path, total_lines: total, start_line: offset, end_line: end, content: content })给每行加行号不是为了好看而是给后续 edit_file 工具提供定位坐标。模型在看到带行号的文件内容后就能在 edit_file 参数里明确写出“从第12行到第15行替换为……”这行话。没有行号的话模型只能用字符串匹配的方式描述修改位置出错率会高很多。默认 limit 我设为200行配合 offset 参数实现分页读取。如果一个文件超过200行Agent 可以第一次读前200行看完后再按需往后读而不是一次性把整个大文件塞进上下文。文件过大时建议让 Agent 先执行一条 grep 命令定位关键代码的位置再定向读取那一段效率比通读全文高得多。4.3 edit_file让 Agent 拥有“笔”编辑文件是这一天里最“危险”的工具因为改错了可能直接破坏项目。我第一版 edit_file 很简单模型提供旧字符串和新字符串代码找到旧字符串替换。但实测问题很大——代码文件里有大量相似片段模型写的旧字符串稍微有一行不符替换就失败或者更糟替换到了不该替换的位置。第二版我改用行号定位稳定了很多def edit_file(path: str, start_line: int, end_line: int, replacement: str): abs_path os.path.abspath(os.path.join(PROJECT_ROOT, path)) if not abs_path.startswith(PROJECT_ROOT): return json.dumps({error: 路径越界}) with open(abs_path, r, encodingutf-8) as f: lines f.readlines() total len(lines) if start_line 1 or end_line total or start_line end_line: return json.dumps({error: f行号越界文件共 {total} 行}) # 保留替换行之前的行和之后的行中间插入新内容 new_lines lines[:start_line-1] [replacement \n] lines[end_line:] with open(abs_path, w, encodingutf-8) as f: f.writelines(new_lines) return json.dumps({success: True, path: path, start_line: start_line, end_line: end_line, replaced_lines: end_line - start_line 1})这个实现的语义简单清晰把 start_line 到 end_line 之间的内容整体替换为 replacement。删除代码时传空字符串就行新增代码时 start_line 和 end_line 设成同一个值。不过行号替换也引入一个新问题文件在 Agent 读取之后、编辑之前可能被外部修改行号漂移会导致改错位置。我的处理是让 Agent 养成“先读后改”的习惯——所有修改操作前必须调用 read_file 拿到最新行号。可以在 edit_file 的描述里明确写上“修改前必须调用 read_file 获取最新文件结构和行号”模型一般会遵守。另外推荐在 edit_file 执行前自动备份原文件比如复制到同目录的 .bak 文件改完后立即对目标文件执行语法检查或运行相关测试让 Agent 自己验证修改是否正确。这一步看起来增加工作量但能避免模型改完代码信心满满地交差、实际项目已经跑不起来的尴尬局面。5. 权限边界与安全防护Agent 有手之后怎么防止闯祸给 Agent 加上工具调用能力的同时也必须给它套上笼头。我在第2天花了整整一个多小时处理安全问题因为一旦 Agent 能执行命令它可以做的事远远超出“修改代码”——它甚至能删库、推送代码、访问敏感文件。这片区域可以说是整篇流水账里最容易被跳过、却最不该被跳过的部分。5.1 工作目录隔离Agent 只能活动在“笼子”里第一层防护是工作目录隔离。我要求所有涉及路径的工具在做任何操作之前必须先做路径规范化校验import os PROJECT_ROOT os.path.abspath(/path/to/project) def safe_path(path: str) - str: abs_path os.path.abspath(os.path.join(PROJECT_ROOT, path)) if not abs_path.startswith(PROJECT_ROOT os.sep) and abs_path ! PROJECT_ROOT: raise PermissionError(f路径越界: {path}) return abs_path这里有两个细节容易漏。第一是必须用 os.path.abspath 做规范化否则 ../../etc/passwd 这类路径会绕过检查。第二是 startswith 判断时要在项目根目录后加上 os.sep否则 /path/to/project-evil 这种路径也会被误判为合规。提示路径校验永远是改文件、删文件前最前一道“闸门”。这道闸门把住之后你的 Agent 再乱也不会把项目外的东西改坏。5.2 危险命令拦截比黑名单更重要的是规则命令执行工具的安全防护比文件路径复杂得多。黑名单方案把 rm、mkfs、dd 等命令封掉看着简单但总有无穷无尽的新花样能绕过。比如 rm -rf ~ 不在黑名单里照样毁掉用户目录。我的方案是做“命令行为检查器”基于规则匹配而非简单名单DANGEROUS_PATTERNS [ r\brm\s-[a-z]*r[a-z]*f, # rm -rf r\bmkfs, # 格式化 r:\(\)\s*\{.*\};, # fork炸弹 r\bdd\sif.*of/dev/, # dd 写设备 r^sudo\srm, # sudo rm ] def check_command(command: str): for pattern in DANGEROUS_PATTERNS: if re.search(pattern, command): raise ValueError(f危险命令被拦截: {command})这套规则不追求百分百完美拦截目标是挡住“大概率是事故”的命令。另外我加了一条硬规则任何涉及 git push 或类似会改变远程状态的操作默认拒绝执行并返回提示让用户手动确认后重试。模型很听话看到提示后会停下来询问用户而不是绕过规则继续。5.3 输出管控与运行环境隔离最后是输出和环境的兜底。我在 exec_command 里加了三个硬限制超时切断任何命令超过 timeout 秒立即终止默认30秒防止 Agent 启动一个永不退出的服务把整个循环卡死输出截断stdout 单次返回不超过2万字符stderr 不超过5000超出部分直接丢弃并在结果中标记工作目录锁定所有命令的 cwd 固定为项目根目录环境变量由程序统一注入而不是让模型随意设置这三个限制各自堵住一类问题。超时切断防“卡死”输出截断防“爆上下文”目录锁定防“越界执行”。安全不是单点设计而是一层层兜底的结果。6. 实测过程与踩坑记录理论讲完代码写完最终还是得上真家伙跑一遍。我在第2天下午用一个小型 Python 项目做了三轮实测第一轮跑的很顺利后面两轮分别暴露了不同的问题。这节把这三轮实测的过程和问题排查经验完整记录下来。6.1 第一轮实测让 Agent 自己定位并修复一个编译错误我准备了一个故意藏有 bug 的 Python 脚本 mini_demo.py模拟某模块配置文件里的端口参数错误导致程序启动失败。我给 Agent 的指令是项目启动报错报错信息说有参数不符合预期。请先运行启动命令复现问题再定位到对应代码文件修复后重新启动确认修复成功。Agent 的工具调用序列记录如下exec_command 运行 python mini_demo.py → 看到报错内容定位到 配置文件 相关逻辑read_file 读取 配置文件 → 发现端口参数被写成了字符串类型edit_file 将字符串改为整数 8080 → 返回成功exec_command 重新运行 python mini_demo.py → exit_code 0启动成功输出最终总结定位到参数类型错误并已修复整个流程跑了4次工具调用耗时约40秒全程无需人工介入。这个结果让我挺兴奋因为它验证了“感知-决策-行动”闭环确实通了Agent 能自己感知报错、决策怎么改、执行修改、验证结果。6.2 第二轮实测暴露的问题工具结果太长导致模型“忘了”任务第二轮我让 Agent 分析一个项目的依赖冲突问题。它执行了 pip 安装命令输出超级长虽然我设了2万字符截断但这条输出加上后续几轮工具结果很快就把对话历史撑大了。问题在第三次工具调用后暴露模型开始输出一些没有信息量的重复内容甚至把用户原始指令里的要求改了——本来只需要“列出冲突”它却开始自行决定去改文件。排查后发现原因是上下文里关于“当前任务目标”的早期用户消息被后续大量工具输出“稀释”了模型的注意力被带偏。我的解决办法是在每轮循环里把用户原始指令重新注入为一条 system-level 的 reminder提醒模型“你被要求完成的原始目标是 X”。这样即使中间环节信息量很大模型也始终有一根“锚”拉住任务主线。改进后重新测试模型不再跑偏。实操心得工具调用轮的上下文管理远比想象中重要。长输出会稀释任务目标建议在每一轮请求里都显式带上原始任务描述或者定期提醒模型“你已经完成了A和B还剩C要做”。6.3 第三轮实测edit_file 把文件改坏了第三轮我踩了一个印象深刻的坑。Agent 修完一个功能后紧接着又要改另一个函数。它第一次 read_file 时文件有120行第二次 edit_file 时它直接使用了第一次读取的行号但第一次修改已经让文件结构发生了变化——代码行号整体偏移了。结果它把原本是正确的代码替换成了错误的片段改动一片混乱。这个问题在上一节其实已经提示过但实际跑的时候模型不一定按预期执行“先读后改”的纪律。我的加固方案有两层在 edit_file 的描述里加粗强调“修改前必须调用 read_file且必须以最近一次 read_file 返回的行号为准”在程序侧检测如果此次 edit_file 的目标文件在最近10分钟内没有被 read_file 读过直接返回警告拒绝执行提示模型先读取两层防护叠加后后续几轮测试没有再出现行号偏移导致的误改。这也是一个很重要的经验Agent 的可靠性不能只靠模型的自觉性还要靠程序侧主动校验来兜底。6.4 故障速查工具调用最常见的6个问题整个第2天实测下来我遇到的典型问题基本可以归纳成一个速查表贴在下面方便你直接对照排查现象根因解决方案API 报 tool_call_id 不匹配tool 消息的 id 和 assistant 消息里的 tool_call.id 不一致每次执行工具时记录原始 id回传时原样使用模型完全忽略工具结果tool 消息没有追加到 messages 或者追加顺序错误确保 assistant tool_calls 消息在 tool 消息之前且完整保留模型反复调用同一个工具工具描述不够明确模型不知道何时结束在描述里增加“满足条件时不应继续调用”的约束命令执行超时卡死整个循环没有给 subprocess 设置 timeout每个 exec_command 强制设 timeout并对长任务分步执行修改文件后项目被改坏编辑前没有读取最新文件结构行号错误程序侧强制“先读后改”并校验改动前后内容差异多轮调用后模型忘掉原始任务工具输出挤占了早期用户指令的注意力每轮请求里重新注入原始任务描述作为 reminder6.5 调参经验tool_choice 怎么设置最后聊一个容易被忽略的调参项tool_choice。大部分框架默认是 auto意思是模型自行决定是否调用工具。实测下来 auto 模式在多数场景表现良好但偶尔模型会在不需要工具的时候强行调用——比如用户只是问“这个项目的代码规范是什么”模型却去执行一条 grep 命令。如果你想做更精细的控制tool_choice 还可以设置为 required强制模型每次必须调用工具或者指定具体工具名强制调用某个工具。第2天的项目中我全程用 auto因为任务类型多样需要保持灵活性。后面做特定场景优化时再考虑用 required。还有一个相关的参数是 temperature。工具调用过程我建议把 temperature 调到 0 或接近 0模型的调用准确率会有明显提升。原理很简单temperature 越高模型越“发散”在需要精确输出 JSON 参数的场景下发散就意味着更大概率生成无效参数或错误工具名。7. 第2天做完后的几点体会这天做完最大的感受是工具调用并不难难的是让“工具调用”稳定可靠地服务于真实任务。原理层面的东西——工具定义、循环、消息序列——看一两篇博客就能懂但实际跑起来你会在意想不到的地方发现问题。比如上下文膨胀对任务目标的稀释、行号漂移导致误改文件、模型在复杂命令前“不知所措”……这些问题没有哪篇官方文档会告诉你只能靠自己在实测中一个一个踩过去。我这篇文章写下的这些代码、参数和排查记录就是希望帮你把这些坑提前踩掉。我个人在实际操作中最受用的一招还是“程序侧强制校验兜底”。不要指望模型每次都严格按提示词里的要求行事而是在关键环节比如修改文件、执行危险命令由代码主动拦截不规范操作。Agent 模型负责创造力和路线规划你写的程序负责纪律和安全各司其职整套系统才靠得住。另外说一个第2天额外获得的小收获给 Agent 加上工具能力之后它在调试代码时的行为模式开始变得像一个初级程序员了——先复现、再定位、然后改、最后验证。这个“行为涌现”并不是哪里硬编码进去的而是工具调用给了模型一个可以施展的空间它的训练数据里关于程序员工作方式的知识才终于有机会映射出来。后面做更复杂的 Agent 能力时这个发现值得持续深挖。明天第3天我计划在现有工具基础上加入代码检索能力和上下文压缩机制让 Agent 面对更大的项目时依然能保持高效和精准。到时候再接着分享实操记录。