多工具Agent:专用工具让AI更稳更安全
从单工具到多工具先回忆上一章的核心循环用户消息进入messages。模型返回普通回答或者返回tool_calls。Harness 根据tool_call.function.name找到真实函数。Harness 执行工具并把结果作为role: tool写回messages。模型看到工具结果后继续推理。这个循环不需要因为工具变多而改变。真正需要扩展的是两部分工具实现真实的 Python 函数比如read_file()、write_file()。工具说明传给模型的TOOLS告诉模型工具名称、用途和参数结构。多工具 Agent 的关键不是让模型“执行更多东西”而是让 Harness 能按工具名稳定分发到不同处理函数。为什么不要只靠 bash既然已经有bash为什么还要单独定义读取文件、写入文件、列目录这些工具因为bash太强也太宽。它既能ls也能cat还能做很多危险动作。对于模型来说一个宽泛工具虽然“自由”但也意味着参数空间巨大、失败模式复杂、安全边界模糊。相比之下专用工具更适合 Agentread_file(path, limit)的意图比bash(sed -n ...)清楚write_file(path, content)的参数结构比拼 shell 命令稳定edit_file(path, old_text, new_text)更容易做安全检查list_dir(path)的输出可以控制格式减少模型解析成本。工具不是越万能越好。很多时候工具越窄Agent 越稳。先定义几个文件工具这一篇先在上一章bash的基础上再定义四个文件类工具工具作用list_dir列出目录内容read_file读取文件内容write_file写入或创建文件edit_file按旧文本替换一处内容代码如下import jsonimport osimport subprocessfrom pathlib import Pathfrom typing import Any, Callablefrom openai import OpenAIclient OpenAI(base_urlhttp://localhost:8080/v1,api_keyno-need,)MODEL gpt-oss-20b-Q4_K_M.ggufWORKDIR Path(os.getcwd()).resolve()def bash(command: str) - str:Execute a bash command and return the output.dangerous_keywords [rm, sudo, shutdown, reboot, init, poweroff]if any(keyword in command for keyword in dangerous_keywords):return Error: command contains dangerous keywords.try:result subprocess.run(command,shellTrue,cwdWORKDIR,capture_outputTrue,textTrue,timeout120,)output (result.stdout result.stderr).strip()return output[:5000] if output else 执行完成但没有输出。except subprocess.TimeoutExpired:return Error: command execution timed out.except Exception as exc:return fError: an unexpected error occurred - {exc}def safe_path(path: str) - Path:target (WORKDIR / path).resolve()if not target.is_relative_to(WORKDIR):raise ValueError(fUnsafe path: {path})return targetdef list_dir(path: str .) - str:try:target safe_path(path)if not target.exists():return fError: path does not exist - {path}if not target.is_dir():return fError: path is not a directory - {path}items []for child in sorted(target.iterdir(), keylambda item: item.name):suffix / if child.is_dir() else items.append(f{child.name}{suffix})return \n.join(items) if items else 目录为空。except Exception as exc:return fError: {exc}def read_file(path: str, limit: int 200) - str:try:target safe_path(path)if not target.exists():return fError: file does not exist - {path}if not target.is_file():return fError: path is not a file - {path}text target.read_text(encodingutf-8)lines text.splitlines()if limit 0 and len(lines) limit:lines lines[:limit] [f... ({len(lines) - limit} more lines)]return \n.join(lines)[:50000]except Exception as exc:return fError: {exc}def write_file(path: str, content: str) - str:try:target safe_path(path)target.parent.mkdir(parentsTrue, exist_okTrue)target.write_text(content, encodingutf-8)return fWrote {len(content)} characters to {path}except Exception as exc:return fError: {exc}def edit_file(path: str, old_text: str, new_text: str) - str:try:target safe_path(path)if not target.exists():return fError: file does not exist - {path}if not target.is_file():return fError: path is not a file - {path}content target.read_text(encodingutf-8)count content.count(old_text)if count 0:return fError: text not found in {path}if count 1:return fError: text is not unique in {path}, found {count} matchestarget.write_text(content.replace(old_text, new_text, 1), encodingutf-8)return fEdited {path}except Exception as exc:return fError: {exc}这里保留几个细节。第一safe_path()很重要。模型生成的路径不能直接信任。../../somewhere这种路径如果不处理就可能逃出当前工作目录。这里用resolve()得到绝对路径再用is_relative_to(WORKDIR)确认目标仍在工作目录里。第二read_file()加了limit。Agent 很容易因为一次读取大文件把上下文塞满。文件读取工具最好默认有行数或字节数限制。第三edit_file()要求old_text只出现一次。如果旧文本出现多次直接替换第一处可能会改错位置。这个限制虽然保守但对教学版 Agent 来说更容易解释也更安全。第四bash仍然只是教学示例。真实项目里不要只靠关键词过滤来保护 shell 工具。文件类工具的第一层安全边界是把所有路径限制在工作目录内。把工具说明给模型实现了真实函数之后还要扩充TOOLS。这一步和上一篇完全一样工具定义不是给 Python 解释器看的而是给模型看的。TOOLS [{type: function,function: {name: bash,description: Execute a shell command and return the output.,parameters: {type: object,properties: {command: {type: string,description: The shell command to execute.,}},required: [command],},},},{type: function,function: {name: list_dir,description: List files and directories under a path.,parameters: {type: object,properties: {path: {type: string,description: Directory path relative to the current working directory.,}},},},},{type: function,function: {name: read_file,description: Read a UTF-8 text file with an optional line limit.,parameters: {type: object,properties: {path: {type: string,description: File path relative to the current working directory.,},limit: {type: integer,description: Maximum number of lines to return. Defaults to 200.,},},required: [path],},},},{type: function,function: {name: write_file,description: Write UTF-8 text content to a file, creating parent directories if needed.,parameters: {type: object,properties: {path: {type: string,description: File path relative to the current working directory.,},content: {type: string,description: The full text content to write.,},},required: [path, content],},},},{type: function,function: {name: edit_file,description: Edit a file by replacing one unique old_text occurrence with new_text.,parameters: {type: object,properties: {path: {type: string,description: File path relative to the current working directory.,},old_text: {type: string,description: The exact text to replace. It must appear exactly once.,},new_text: {type: string,description: The replacement text.,},},required: [path, old_text, new_text],},},},]工具定义有两个方向要对齐。一边要对齐模型name、description、parameters要足够清楚让模型知道什么时候该选哪个工具。另一边要对齐 Harness工具名必须能映射到真实处理函数参数名也要和处理函数的入参一致。比如read_file的 schema 里有path和limit真实函数也最好就是read_file(path: str, limit: int 200)。第一版用 if/elif 分发工具工具变多之后最直接的写法是在 Agent Loop 里判断工具名for tool_call in assistant_message.tool_calls:arguments json.loads(tool_call.function.arguments)if tool_call.function.name bash:tool_result bash(arguments[command])elif tool_call.function.name list_dir:tool_result list_dir(arguments.get(path, .))elif tool_call.function.name read_file:tool_result read_file(arguments[path], arguments.get(limit, 200))elif tool_call.function.name write_file:tool_result write_file(arguments[path], arguments[content])elif tool_call.function.name edit_file:tool_result edit_file(arguments[path],arguments[old_text],arguments[new_text],)else:tool_result fError: unknown tool {tool_call.function.name}这个版本能跑也很直观。但问题也明显每增加一个工具都要改 Agent Loop工具分发逻辑和循环控制逻辑混在一起参数解析和错误处理会重复工具多了以后if/elif会越来越长。Agent Loop 是 Harness 的核心路径不应该因为新增一个普通工具就频繁改动。更好的方式是把“工具名 - 处理函数”的关系独立出来。第二版用 TOOL_HANDLERS 注册工具我们可以增加一个工具注册表TOOL_HANDLERS: dict[str, Callable[..., str]] {bash: bash,list_dir: list_dir,read_file: read_file,write_file: write_file,edit_file: edit_file,}这个映射表的含义很直接如果模型请求调用read_fileHarness 就从TOOL_HANDLERS[read_file]里取出真实函数然后把解析出来的参数传进去。这样做之后可以把单个工具调用的执行过程封装成一个函数def run_tool_call(tool_call: Any) - str:name tool_call.function.namehandler TOOL_HANDLERS.get(name)if handler is None:return fError: unknown tool {name}try:arguments json.loads(tool_call.function.arguments or {})except json.JSONDecodeError as exc:return fError: invalid JSON arguments for {name} - {exc}if not isinstance(arguments, dict):return fError: arguments for {name} must be a JSON object.try:return handler(**arguments)except TypeError as exc:return fError: invalid arguments for {name} - {exc}except Exception as exc:return fError: tool {name} failed - {exc}这段代码承担了四件事根据工具名找到处理函数把模型生成的 JSON 字符串解析成 dict确认参数结构是 JSON object执行工具并把异常转换成模型能读懂的文本结果。这里再次强调parameters能帮助模型生成更接近正确的参数但它不等于运行时安全。Harness 仍然要做解析、校验和错误处理。改造后的 Agent Loop有了run_tool_call()之后Agent Loop 就能保持和上一篇几乎一样的结构SYSTEM_PROMPT f你是一个编程助手工作在当前目录{WORKDIR}。你可以使用工具读取文件、列出目录、写入文件、编辑文件和执行必要的 shell 命令。调用工具前先判断需要什么信息拿到工具结果后再决定是否继续调用工具或给出最终回答。def run_loop(user_message: str, max_steps: int 8) - str:messages: list[dict[str, Any]] [{role: user, content: user_message}]for _ in range(max_steps):response client.chat.completions.create(modelMODEL,messages[{role: system, content: SYSTEM_PROMPT}] messages,toolsTOOLS,)assistant_message response.choices[0].messageif not assistant_message.tool_calls:final_answer assistant_message.content or messages.append({role: assistant, content: final_answer})return final_answermessages.append({role: assistant,content: assistant_message.content or ,tool_calls: [{id: tool_call.id,type: function,function: {name: tool_call.function.name,arguments: tool_call.function.arguments,},}for tool_call in assistant_message.tool_calls],})for tool_call in assistant_message.tool_calls:tool_result run_tool_call(tool_call)messages.append({role: tool,tool_call_id: tool_call.id,content: tool_result,})return 达到最大循环次数任务仍未完成。if __name__ __main__:user_input input(Enter your message: )output run_loop(user_input)print(Response from LLM:, output)注意这里仍然保留了上一篇的几个关键点没有tool_calls时说明模型给出了最终回答有tool_calls时先把带有tool_calls的assistant消息写入历史每个工具执行结果都用role: tool写回tool_call_id必须对应原来的tool_call.idmax_steps防止循环失控。变化只有一个Agent Loop 不再关心具体有哪些工具它只负责遍历模型请求的工具调用然后交给run_tool_call()。一轮里出现多个工具调用怎么办上一篇已经提到过assistant_message.tool_calls是一个列表。也就是说模型一轮里可能请求多个工具调用。比如它可能同时请求list_dir({path: .})read_file({path: README.md, limit: 80})Harness 的处理方式很简单遍历这个列表为每个tool_call执行一次工具并追加一条对应的tool消息。这里最重要的是保持对应关系for tool_call in assistant_message.tool_calls:tool_result run_tool_call(tool_call)messages.append({role: tool,tool_call_id: tool_call.id,content: tool_result,})如果有两个工具调用就应该追加两条tool消息。每条tool消息都用自己的tool_call_id指回对应的请求。不要把多个工具结果随便合并成一条普通文本否则模型很难判断哪个结果对应哪个调用。新增工具时要做什么有了工具注册表之后新增工具的步骤变得很固定。假设后面要增加一个http_get(url)工具大致只需要做三件事定义真实函数http_get(url: str) - str在TOOLS里添加工具说明让模型知道它可以调用在TOOL_HANDLERS里添加http_get: http_get。Agent Loop 不需要改。这就是注册表的价值工具扩展发生在边缘循环控制保持稳定。当然真实项目里可以继续往前走把TOOLS和TOOL_HANDLERS合并成更完整的工具对象。例如每个工具都包含工具名工具描述参数 schema处理函数权限等级是否需要用户审批返回结果最大长度超时时间。这时工具系统就不只是一个 dict而会慢慢变成 Harness 里的一个核心模块。小结这篇在上一篇最小 Agent Loop 的基础上继续做了三件事定义多个专用工具让 Agent 不再只依赖bash用TOOLS把每个工具的名称、描述和参数结构告诉模型用TOOL_HANDLERS把工具名映射到真实处理函数让 Agent Loop 保持稳定。到这里我们已经有了一个更像样的工具系统雏形模型负责选择工具Harness 负责校验参数、执行函数、回传结果。

相关新闻

Hello-World项目架构解析:60+语言文件的组织与管理技巧

Hello-World项目架构解析:60+语言文件的组织与管理技巧

Hello-World项目架构解析:60语言文件的组织与管理技巧 【免费下载链接】Hello-World Hello World in all possible programmnig languages 项目地址: https://gitcode.com/gh_mirrors/hellowo/Hello-World Hello-World项目是一个旨在汇集多种编程语言实现&qu…

2026/8/10 21:42:53 阅读更多 →
50个Dify工作流模板:零基础也能上手的AI自动化工具箱

50个Dify工作流模板:零基础也能上手的AI自动化工具箱

50个Dify工作流模板:零基础也能上手的AI自动化工具箱 【免费下载链接】Awesome-Dify-Workflow 分享一些好用的 Dify DSL 工作流程,自用、学习两相宜。 Sharing some Dify workflows. 项目地址: https://gitcode.com/GitHub_Trending/aw/Awesome-Dify-W…

2026/8/10 21:41:53 阅读更多 →
【无人机三维路径规划】基于THRO GGO TOC PGA IVY CCO TGCOA多种算法实现城市空中交通多无人机路径规划MATLAB代码

【无人机三维路径规划】基于THRO GGO TOC PGA IVY CCO TGCOA多种算法实现城市空中交通多无人机路径规划MATLAB代码

✅作者简介:热爱科研的Matlab仿真开发者,擅长毕业设计辅导、数学建模、数据处理、算法改进、程序设计科研仿真。🍎 往期回顾关注个人主页:完整代码获取 定制创新 论文复现私信🍊个人信条:做科研&#xff0c…

2026/8/10 21:41:53 阅读更多 →

最新新闻

UE5编译错误LNK1181:彻底解决找不到delayimp.lib问题

UE5编译错误LNK1181:彻底解决找不到delayimp.lib问题

1. 项目概述:当UE5编译时告诉你“找不到delayimp.lib”如果你正在用UE5开发C项目,或者尝试创建一个新的插件,突然在Visual Studio的编译输出里看到一行刺眼的红色错误:“LINK : fatal error LNK1181: 无法打开输入文件‘delayimp.…

2026/8/11 6:54:19 阅读更多 →
2027 年商城系统开发商公司实力排名预测|私有化源码与 SaaS 赛道深度分析

2027 年商城系统开发商公司实力排名预测|私有化源码与 SaaS 赛道深度分析

摘要:基于 2026 年电商行业现状,从技术架构、源码开放度、B2B2C 多商户、资金分账合规、AI 能力、落地案例、售后迭代七大维度,预测 2027 国内商城系统厂商格局,分为私有化源码交付、SaaS 云租用两大赛道,给企业技术选…

2026/8/11 6:54:19 阅读更多 →
GRUB启动项管理:从原理到实战,解决冗余与引导失效问题

GRUB启动项管理:从原理到实战,解决冗余与引导失效问题

1. 项目概述:为什么GRUB启动项会“冗余”? 如果你在电脑上安装了Ubuntu,尤其是和Windows组成了双系统,那么开机时看到的那个黑底白字的菜单,就是GRUB。GRUB全称是GRand Unified Bootloader,是Linux世界里最…

2026/8/11 6:54:19 阅读更多 →
同行在打价格战,做齿轮箱加工的我干了件别的事

同行在打价格战,做齿轮箱加工的我干了件别的事

前几天算了一笔账,这个月房租都不够。机子停三台,人也闲了俩。 我开这家齿轮箱加工厂七年了,六台数控车床加两台滚齿机,十二个工人,专做注塑机和包装机上的中小模数齿轮箱。往年淡季是七八月份,今年三月份就…

2026/8/11 6:54:19 阅读更多 →
ARM架构下Nginx源码编译优化指南:从指令集调优到系统集成

ARM架构下Nginx源码编译优化指南:从指令集调优到系统集成

1. 项目概述与核心需求解析最近在折腾一台基于ARM架构的工控板,想把它变成一个轻量级的Web服务器,用来跑几个内部的管理页面和API接口。第一反应就是上Nginx,毕竟它轻量、高效、配置灵活,是Linux服务器上的“瑞士军刀”。但这次和…

2026/8/11 6:54:19 阅读更多 →
第9课:冲突管理——从对抗到对话的转化技术

第9课:冲突管理——从对抗到对话的转化技术

第9课:冲突管理——从对抗到对话的转化技术 章节导语 你一定见过这样的场景—— 两个核心成员在技术方案上产生严重分歧:一个坚持用微服务架构,另一个坚持用单体架构。双方各执一词,谁也说服不了谁。更糟糕的是,团队开…

2026/8/11 6:53:18 阅读更多 →

日新闻

如何用Video2X实现专业级视频画质提升:AI视频增强完整指南

如何用Video2X实现专业级视频画质提升:AI视频增强完整指南

如何用Video2X实现专业级视频画质提升:AI视频增强完整指南 【免费下载链接】video2x A machine learning-based video super resolution and frame interpolation framework. Est. Hack the Valley II, 2018. 项目地址: https://gitcode.com/GitHub_Trending/vi/v…

2026/8/11 0:00:02 阅读更多 →
前后端分离项目中控制台与接口工具数据差异排查指南

前后端分离项目中控制台与接口工具数据差异排查指南

1. 问题现象解析:控制台与Apifox的数据差异 最近在调试一个前后端分离项目时,遇到了一个典型问题:后端服务在本地开发环境控制台能正常输出查询数据,但通过Apifox测试时却返回空结果。这种"控制台有数据,接口工具…

2026/8/11 0:00:03 阅读更多 →
AI编程实战:从Claude Code踩坑到游戏开发入门

AI编程实战:从Claude Code踩坑到游戏开发入门

1. 从“AI能帮我做游戏”到“AI让我重新学编程”最近身边不少朋友,尤其是一些非技术背景、但对游戏开发有浓厚兴趣的朋友,都在问我同一个问题:“听说现在用Claude Code这种AI编程工具,小白也能做游戏了,是真的吗&#…

2026/8/11 0:00:03 阅读更多 →

周新闻

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁

5分钟告别提取码焦虑:baidupankey如何智能破解百度网盘资源锁 【免费下载链接】baidupankey 在线查询网盘提取码(维护中 rm repo) 项目地址: https://gitcode.com/gh_mirrors/ba/baidupankey 你是否曾经在深夜寻找一份重要资料&#x…

2026/8/11 1:08:05 阅读更多 →
如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南

如何快速生成中国车牌图片:Python开源工具完整指南 【免费下载链接】chinese_license_plate_generator 中国车牌生成器 项目地址: https://gitcode.com/gh_mirrors/ch/chinese_license_plate_generator 中国车牌生成器是一个基于Python的开源项目&#xff0c…

2026/8/11 1:08:05 阅读更多 →
收藏!小白程序员轻松入门大模型,从Harness工程开始实践

收藏!小白程序员轻松入门大模型,从Harness工程开始实践

文章强调学习大模型不应只关注模型本身,而应重视模型外的系统搭建,即Harness。提出AgentModelHarness的实用公式,详细介绍Harness的四个层次:持久化层、执行层、控制层和观察与验证层。文章还探讨了上下文工程、工具设计、AGENTS.…

2026/8/11 1:08:05 阅读更多 →

月新闻

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南

免费解锁百度网盘SVIP加速:macOS用户必备的下载提速终极指南 【免费下载链接】BaiduNetdiskPlugin-macOS For macOS.百度网盘 破解SVIP、下载速度限制~ 项目地址: https://gitcode.com/gh_mirrors/ba/BaiduNetdiskPlugin-macOS 还在为百度网盘macOS版的龟速下…

2026/8/10 17:07:33 阅读更多 →
终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换

终极ncmdump指南:3分钟实现网易云NCM音乐解密与格式转换 【免费下载链接】ncmdump 项目地址: https://gitcode.com/gh_mirrors/ncmd/ncmdump 还在为网易云音乐下载的NCM格式文件无法在其他播放器播放而烦恼吗?ncmdump解密工具帮你轻松解决这个困…

2026/8/11 1:08:06 阅读更多 →
HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

HarmonyOS 应用开发《掌上英语》第81篇: 智能体卡片:为英语学习 App 打造桌面级学习助手

AgentCard 智能体卡片:为英语学习 App 打造桌面级学习助手适用平台:HarmonyOS 7.0 (API 26 Beta)一、引言 HarmonyOS 7.0(API 26 Beta)新增了 AgentCard 智能体卡片能力,这是继 HMAF(鸿蒙智能体框架&#x…

2026/8/10 17:07:33 阅读更多 →