
说干就干把项目文件夹直接命名成了HelloAgentsLLM扩展版本号一路踩到7.2。朋友看到这个标题问我到底扩展了啥我说你就把它当成给大模型装了一套扩展坞——模型原本只会打字回答你接上这套东西之后它能自己调计算器、查网页、读文件再把结果揉进回答里给你。这个项目本质就是解决一个问题让LLM从会说话变成会办事。如果你最近在折腾Agent、RAG或者任何想让大模型真正干活的项目这篇内容应该能帮上忙。我会把7.2版本的架构思路、工具注册机制、Agent运行循环、安全加固方式全部拆开讲最后附上实操过程和三段可直接复用的代码骨架。只要你用的模型服务支持函数调用Function Calling这套扩展玩法基本都能照搬。1. 项目由来为什么聊天机器人需要长出手脚先说背景。我做这个项目之前手头跑着一个很标准的纯对话脚本用户提问模型回答上下文存在内存里聊完即焚。这种脚本当玩具还行真放到实际场景里痛点马上就冒出来了。用户问帮我算一下这个户型贷款月供多少模型开始一本正经编数字用户说看看这个目录下最新一份报告讲了什么模型回答我无法访问本地文件。说白了模型脑袋里装的是训练时的知识它没有手、没有眼睛也没办法替你操作外部世界。后来我试着把这些需求硬塞进System Prompt比如告诉模型你应该想象自己是一个助手你可以用计算公式……结果就是模型开始幻觉工具、幻觉计算结果看着像那么回事实际一个数都不能信。Prompt里塞能力这条路根本走不通——它没有真正的执行能力只是在模仿有执行能力的样子。于是我把目光转向Function Calling机制。这个机制说白了就是你在API请求里附带一份工具清单模型根据用户问题决定要不要调用某个工具需要调用时输出一段结构化的调用指令工具名参数然后由你写的代码真正去执行这段指令再把结果喂回给模型。整个过程模型负责思考决定你的代码负责实际动手。7.2版本的核心改动就是这么来的把原本死板的对话循环改造成了一个带工具总线的Agent引擎所有外部能力通过扩展的方式挂到总线上。项目命名为HelloAgents也是想保持 HelloWorld 那种探路工程的心态——先跑通最小闭环再不断往上叠能力。这个项目适合三类人一是刚入坑Agent开发、想搞清楚工具调用到底怎么回事的新手二是手里有现成LLM应用、想给它加搜索、加文件操作等能力的开发者三是想做企业级Agent平台、需要一套可扩展工具注册表作为底座的人。看完这篇你能带走一套可以直接抄的工具注册框架以及我迭代7个版本踩过的所有坑。2. 扩展的整体架构我给LLM造了一个扩展坞计算机领域里的扩展大家早就不陌生了。浏览器装插件CRX文件往浏览器里一加载网页就能多出一堆能力显卡坏了或者想多屏办公买一个扩展坞Type-C口一插外设全亮了更底层一点的I2C扩展就是总线上挂一堆设备主机通过标准协议跟它们通信。这些玩法本质都一样宿主提供标准接口第三方能力插上去就能干活。我给LLM做的这套扩展体系遵循的也是同一个逻辑。LLM是宿主工具是外设Agent循环是总线协议。我只需要规定好外设长什么样、怎么上报能力、怎么被调用剩下的事情就可以无限叠加——今天接一个计算器明天接一个搜索引擎后天再接一个文件读取器互不干扰随时插拔。2.1 扩展的本质从模型输出到模型行动扩展之前模型的工作流只有一步用户发消息 → 模型返回文本。整个过程是单向的模型对世界没有任何影响。扩展之后工作流变成了闭环停顿用户发消息 → 模型决定要不要调用工具 → 如果要就输出一段结构化调用请求 → 程序执行工具拿到结果 → 把结果作为新消息喂回模型 → 模型基于真实结果继续回答。这个循环可以反复进行直到模型认为信息足够、输出最终答案为止。这一段小小的流程变化就是Agent与普通ChatBot的分水岭。ChatBot只会说Agent干完还说。而扩展机制是这个转变里最关键的一层——它把模型输出的文字指令翻译成真实世界里的操作再把这些操作的结果翻译回模型能理解的文本。2.2 分层设计宿主、注册表、调度器、安全闸门在7.2版本里我把整个扩展体系拆成了四个层级代码里分别对应四个模块层级职责关键点宿主核心维护对话循环、管理上下文决定什么时候该调工具扩展注册表登记所有可用工具的元信息决定有哪些工具可调调度器解析模型的调用意图并执行决定工具拿什么参数跑安全闸门校验参数、控制权限、超时熔断决定工具能不能这么跑这套分层一开始就定下来了后面迭代时省了非常多事。你可能会想小项目有必要这么分层吗我自己实践下来的答案是有必要而且越早越好。最初几版我把注册和执行合在一起每加一个新工具就要复制粘贴一大段调度代码出问题的时候完全分不清是注册表写错了还是执行函数写崩了。拆开之后每层都可以单独测试、单独替换加新扩展只需要写一个工具类然后登记进注册表其他什么都不要动。2.3 为什么不做能力缝合而是做可插拔扩展也有朋友问过我你这些能力为什么不直接在System Prompt里写清楚非要搞一套扩展注册机制这个问题问到了点子上。Prompt缝合的方式在工具只有一两个的时候确实简单把遇到计算题就输出公式写进提示词让模型自己算个大概。但工具一旦多起来所有能力描述挤在提示词里会互相干扰模型会搞混什么时候该用哪个工具而且你没法对每个工具单独做权限控制——总不能因为用户问了句读取某个文件就把整个文件系统的能力都开放给模型。扩展机制的优势正好在这里第一能力描述独立存储按需注入当前请求不会互相污染第二每个工具自带校验逻辑和权限标记归档管理第三工具支持独立升级、独立下线不用动主代码。后来我把项目从个人脚本扩展成团队共用服务时这种设计几乎没花成本就完成了多人的工具接入。谁要加工具谁就实现一个注册类跑一下测试用例合并即可主流程完全不用我操心。3. 核心细节拆解工具注册表与Agent循环的实现要点知道大概架构了接下来是最关键的部分——这些机制落到代码上到底怎么写。这一节不会贴一整套完整项目而是把最核心的几个模块拆出来告诉你每个模块解决的问题以及我为什么这么设计。3.1 工具Schema模型的操作说明书任何函数调用型模型都需要一份工具清单来描述你可以用什么。这个清单的格式基本都遵循OpenAI的规范一个工具包含名字、描述、以及参数结构。参数结构用JSON Schema描述告诉模型这个工具需要哪些参数、参数是什么类型、哪些必填。写工具Schema我总结了三条经验第一条description是灵魂。模型能不能正确调用工具七成靠描述写得好不好。不要说计算工具要说计算数学表达式支持加减乘除和幂运算输入格式为普通数学表达式如 (12000 * 0.045) / 12。模型是靠语言理解触发条件的描述越具体触发准确率越高。第二条参数要严但不苛刻。必填参数设为required可选项给默认值。很多模型对空值处理不稳定宁可让参数类型严格一点也不要允许null乱入。第三条工具数量要克制。一次请求注入5到8个工具描述是比较舒服的量级塞个三四十个工具模型决策时间变长、失误率明显上升。工具多了要做路由但那是后话小项目先用数量控制。3.2 注册表用一个字典挂起全世界在Python工程里我习惯用一个小型元类机制实现工具注册思路类似Flask的路由表维护。每个工具类都继承自一个BaseTool基类声明name、description、schema以及execute方法然后通过装饰器自动注册。如果不想用元类用一个简单的全局字典手动注册也完全够用。重点不是机制的花哨程度而是你要保证注册表里的工具名唯一并且与Schema里的name严格一致。我调试初期遇到最多的怪问题就是注册表登记名和Schema名字少了一个字母模型倒是认真调用了调度器却找不到执行入口。3.3 Agent循环决定要不要动手的那个引擎Agent循环是整个扩展体系的发动机它干的事情就是反复执行拼请求 → 调API → 看返回 → 做判断。我用伪代码描述一下这个循环的核心路径实际代码你往下翻第4节。核心逻辑拆开就是这几步把系统提示词、多轮对话历史、当前工具清单一起发给模型服务判断返回内容里有没有tool_calls字段如果没有说明模型认为不用调工具了直接把内容返回给用户循环结束如果有工具调用请求遍历每一个调用解析参数执行调度器拿到执行结果把每个工具调用及其结果以tool角色的消息追加进对话历史带着更新后的完整历史包括工具结果回到第1步继续跑。这里有一个很容易被忽略的细节工具结果必须回填到对话里而且角色必须是tool同时带上对应的tool_call_id。不少初次接触函数调用的朋友忽略了这个关联字段结果就是模型越答越糊涂因为对不上号。迭代次数的控制也在这个循环里。我的做法是设一个MAX_ITERATIONS默认8次。一个Agent任务循环两三步能解出来是正常表现跑到五六步说明路线很绕超过8步大概率是卡死了直接停掉并提示模型总结当前进度。这个参数的设置我后面在常见问题里还会细说。3.4 安全闸门扩展装上之前必须先过安检给LLM接扩展本质上就是把系统操作权交给一个嘴上没把门的模型。做过浏览器扩展开发的朋友知道Chrome如果检测到恶意行为或违反策略会直接禁用扩展LLM的工具扩展也是同一个道理——没有安全检查的执行权限就是定时炸弹。7.2版本里的安全闸门做了三层第一层是参数校验工具执行前用JSON Schema对模型传入的参数做合法性检查防止出现类型错误或者必填缺失。别小看这步模型偶尔会生成一些非常离谱的参数比如把文件名传成了布尔值这种不拦住后面所有工具都会跟着崩。第二层是权限控制每个工具类声明自己需要的权限级别比如文件读取器只能访问白名单目录网络工具只能访问白名单域名。任何超出范围的路径或URL直接拒绝执行并返回错误信息给模型让它换一种方式解决问题。第三层是超时与异常兜底所有工具执行都跑在try-except里并且加上超时限制。工具挂掉不可怕可怕的是工具挂掉以后整个Agent进程崩溃。我的习惯是任何异常都要转成一段错误文本回传模型让模型基于错误信息做下一步决策而不是自己FW崩掉。3.5 上下文管理再好的工具也架不住对话被塞爆工具交互会产生大量额外消息每一轮工具调用都会把调用请求执行结果写进对话历史。如果你在循环里不控制消息量跑个几轮后上下文就膨胀到可怕API账单也跟着膨胀。我处理上下文的核心策略是截断裁剪工具执行结果在回传模型之前先看它的长度超过三五百字符就自动摘要截断。尤其程序输出、网页正文这些东西几百上千行都是常态模型根本不需要看全量源码只需要看关键信息。主对话历史上限也设了一个窗口超过最近20条消息后开始淘汰更早的消息保证模型始终盯着最近的信息做判断。这套上下文策略起初没有加跑通基础功能后我实际用下来发现没有它根本不行——一次搜索返回的内容就足以让模型忘掉之前用户说过什么。后面第5节我还会详细讲排查过程。4. 实操过程把三个扩展从零跑起来理论讲了一堆接下来是动手环节。我会用Python 3.10 openai SDK来演示假设你已经有可用的API Key并且模型服务支持函数调用。这三个扩展我选得特别务实计算器、网络搜索、文件读取——分别是内部计算、外部数据、本地环境三种最典型的扩展类型能覆盖大部分Agent的实际需求。4.1 环境准备与基础代码骨架先安装依赖。我默认你用的是虚拟环境如果还没建就从建虚拟环境开始。python -m venv .venv source .venv/bin/activate # Windows下执行 .venv\Scripts\activate pip install openai requests然后创建主文件agent.py先搭出工具基类和注册表。这是整个项目的地基。# agent.py from abc import ABC, abstractmethod from typing import Any, Dict, List, Optional class BaseTool(ABC): 所有扩展工具的基类。 # 工具元信息子类必须覆盖 name: str description: str parameters: Dict[str, Any] {} required: List[str] [] abstractmethod def execute(self, **kwargs) - str: 执行具体操作返回字符串形式的结果。 raise NotImplementedError def to_openai_tool(self) - Dict[str, Any]: 把工具转成OpenAI函数调用协议里的tool结构。 return { type: function, function: { name: self.name, description: self.description, parameters: { type: object, properties: self.parameters, required: self.required, }, }, } # 全局注册表以字典形式保存全部可用工具 TOOL_REGISTRY: Dict[str, BaseTool] {} def register_tool(tool_cls): 装饰器让工具类自动注册进全局表。 instance tool_cls() TOOL_REGISTRY[instance.name] instance return tool_cls def get_all_tools() - List[Dict[str, Any]]: 返回供API请求使用的工具清单。 return [tool.to_openai_tool() for tool in TOOL_REGISTRY.values()]这个铺垫代码本身没有任何复杂逻辑就是一个协议约定。它最重要的作用是把工具长什么样固定下来后面不管是加AI绘画、加数据库查询、加浏览器操作都只需要实现一个新类不需要改其他任何地方。4.2 扩展一内置计算器——注意别直接裸用eval计算器是第一个扩展也是最适合练手的。功能很简单接收一个数学表达式字符串返回计算结果。但这里有个安全性的经典之选eval()直接执行字符串等于把任意代码执行权交给了模型。模型一旦被注入prompt可能输出__import__(os).system(rm -rf /)之类的内容虽然不是每次都会发生但赌这个太蠢了。我的解法是写一个简易的AST解析求值器白名单只允许数字、四则运算符、括号、幂运算和小数点。这样表达式再怎么复杂也逃不出白名单范围。# tools/calculator.py import ast import operator from agent import BaseTool, register_tool # 允许的运算符白名单 _ALLOWED_OPERATORS { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg, # 一元负号 ast.Mod: operator.mod, } register_tool class CalculatorTool(BaseTool): name calculator description ( 计算数学表达式支持加、减、乘、除、幂运算、取模和括号。 输入参数为普通数学表达式字符串例如 (12000 * 0.045) / 12。 ) parameters { expression: { type: string, description: 要计算的数学表达式, } } required [expression] def _safe_eval(self, node): if isinstance(node, ast.Num): return node.n if isinstance(node, ast.BinOp): op_type type(node.op) if op_type not in _ALLOWED_OPERATORS: raise ValueError(f不允许的运算符: {op_type}) left self._safe_eval(node.left) right self._safe_eval(node.right) return _ALLOWED_OPERATORS[op_type](left, right) if isinstance(node, ast.UnaryOp): op_type type(node.op) if op_type not in _ALLOWED_OPERATORS: raise ValueError(f不允许的一元运算符: {op_type}) operand self._safe_eval(node.operand) return _ALLOWED_OPERATORS[op_type](operand) raise ValueError(f不支持的表达式节点: {type(node)}) def execute(self, expression: str ) - str: tree ast.parse(expression, modeeval) result self._safe_eval(tree.body) return str(result)这段代码值得解释一下。ast.parse不会执行任何代码它只是把字符串解析成语法树我再去遍历这个树只放行白名单里的节点类型。这比正则过滤可靠得多因为AST层面能精确识别出一个节点到底是数字还是函数调用属性访问后者一律拒绝。这个工具虽然简单却很能说明问题扩展在执行前必须假设模型输入不可信。模型生成参数的过程本质上是一种统计采样你不知道它会输出什么鬼东西出来。按不可信输入来设计至少不会出事。4.3 扩展二网络搜索工具——把外部API封装成标准接口第二个扩展是搜索。真实项目的搜索引擎通常是调用搜索API比如某云厂商的网页检索服务或者自建检索引擎。我这里演示的是通用模式用requests库请求一个公开API把返回的JSON整理成精简文本。代码骨架是这样的# tools/websearch.py import json import requests from agent import BaseTool, register_tool register_tool class WebSearchTool(BaseTool): name web_search description ( 搜索互联网获取最新信息。当用户问到新闻、实时数据、不熟悉的名词时使用。 输入为搜索关键词返回搜索结果标题和摘要列表。 ) parameters { query: { type: string, description: 搜索关键词或问题, }, limit: { type: integer, description: 返回结果条数默认5最多10, }, } required [query] def execute(self, query: str , limit: int 5) - str: # 这里填你自己的搜索API地址与密钥 api_url https://your-search-endpoint.com/v1/query headers {Authorization: Bearer YOUR_API_KEY} try: r requests.get( api_url, headersheaders, params{q: query, limit: min(int(limit), 10)}, timeout10, ) r.raise_for_status() except Exception as e: # 报错也回传模型让模型决定怎么办 return f搜索请求失败: {e} results r.json().get(results, []) lines [] for item in results[: int(limit)]: title item.get(title, ) snippet item.get(snippet, ) url item.get(url, ) lines.append(f- {title}\n {snippet}\n {url}) return \n.join(lines) if lines else 没有找到相关结果这个工具展示的是接入任意第三方服务的通用套路准备好鉴权方式、组装请求、解析响应、整理成一两百字的摘要返回。注意我在返回结果里包含了标题、摘要和链接但没有把全文塞回去因为模型只需要摘要就能理解内容全文会撑爆上下文。实际我后来接天气、汇率、股票行情时用的都是同一套模板改URL、改参数、改解析逻辑其他什么都不动。4.4 扩展三文件读取工具——路径白名单是保命符第三个扩展是读文件。这个功能很危险稍不注意就是任意文件读取漏洞。所以我把安全校验放在工具执行的第一步路径必须经过规范化并且落在白名单目录内。# tools/file_reader.py import os from agent import BaseTool, register_tool # 只允许读取这个目录 ALLOWED_ROOT os.path.expanduser(~/agent_data/) register_tool class FileReaderTool(BaseTool): name read_file description ( 读取本机指定文本文件的全部内容用于分析报告、读取配置、查看日志等。 只能访问受信任目录下的文件非该目录内的路径会返回错误。 输入relative_path为相对受信任目录的文件路径。 ) parameters { relative_path: { type: string, description: 相对于受信任目录的文件路径例如 notes/meeting.md, } } required [relative_path] def execute(self, relative_path: str ) - str: # 拼接并规范化路径防止 ../ 跳目录 abs_path os.path.realpath(os.path.join(ALLOWED_ROOT, relative_path)) if not abs_path.startswith(os.path.realpath(ALLOWED_ROOT)): return 错误不允许读取该路径。 if not os.path.isfile(abs_path): return 错误文件不存在。 try: with open(abs_path, r, encodingutf-8) as f: content f.read() except Exception as e: return f读取失败: {e} # 单次最多返回 2000 字符超出部分截断 return content[:2000] (\n……(内容过长已截断) if len(content) 2000 else )os.path.realpath处理了符号链接和..跳转问题。文件长度限制也在这里生效了——2000字符以内的内容返回给模型超出部分直接截掉。这个截断策略虽然粗暴但对大多数分析场景够用模型很少需要看一份完整的三千行日志。真正生产环境下文件读取建议配合目录列表工具一起用模型先调用列目录选好文件再调用读取。这样链条清晰出错也好定位。4.5 主循环把扩展调度起来工具写好了现在把它们插到Agent循环里。主循环代码不长但每个细节都可能影响成败。# agent.py 续 import json from openai import OpenAI client OpenAI( base_urlhttps://your-llm-endpoint.com/v1, # 换成你的模型服务地址 api_keyYOUR_API_KEY, ) SYSTEM_PROMPT 你是一个拥有工具使用能力的智能助手。请先思考用户的需求需要工具时直接调用再根据工具结果回答。 def process_tool_calls(resp, messages): 执行模型请求的工具调用并把调用过程和结果写回消息列表。 for tool_call in resp.choices[0].message.tool_calls: func_name tool_call.function.name func_args tool_call.function.arguments or {} try: args json.loads(func_args) except json.JSONDecodeError: args {} # 解析失败就用空参数让工具自行纠错 tool TOOL_REGISTRY.get(func_name) if tool is None: result f错误找不到工具 {func_name} else: try: result tool.execute(**args) except Exception as e: result f工具执行异常: {e} # 把模型发起的调用请求写进历史 messages.append({ role: assistant, tool_calls: [ { id: tool_call.id, type: function, function: { name: func_name, arguments: func_args, }, } ], }) # 把执行结果以tool角色写进历史并保持和call id的关联 messages.append({ role: tool, tool_call_id: tool_call.id, content: result, }) return messages def run_agent(user_input: str, max_iterations: int 8) - str: messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_input}, ] for _ in range(max_iterations): resp client.chat.completions.create( modelyour-model-name, messagesmessages, toolsget_all_tools(), tool_choiceauto, ) message resp.choices[0].message # 没有工具调用说明Agent认为可以收尾了 if not message.tool_calls: return message.content or # 有工具调用执行并追加消息 messages.append({ role: assistant, content: message.content or , }) messages process_tool_calls(message, messages) return 达到最大迭代次数任务未能完成请简化需求或检查工具配置。 if __name__ __main__: # 手动验证也可以用命令行参数传入 print(run_agent(帮我算一下月供贷款120万年化利率4.5%期限20年等额本息))这里我特别想说的是tool_call_id的关联。当初第一次写循环时漏了这一步工具结果确实回传了但模型总是莫名其妙地回答我没有调用过这个工具。后来查了文档才发现tool角色的消息必须携带对应的tool_call_id模型才能把工具执行和它的调用意图精确对上。这一步错位整个循环就废了。4.6 实际跑出来的效果我拿计算器那个问题实测了一下效果大致长这样用户帮我算一下月供贷款120万年化利率4.5%期限20年等额本息第一轮模型返回一个tool_calls调用calculator参数是月利率公式表达式调度器执行计算器返回结果第二轮模型看到计算结果继续组织语言输出按等额本息计算月供约为7593.30元整个过程不到三秒模型每一步都有据可依。这种感觉和之前模型一本正经瞎编月供数字完全是两回事。这个体验上的差距就是Function Calling机制最直接的价值。5. 常见问题与排查技巧实录7.2版本走到这一步其实是从1.0踩坑踩过来的。下面这些问题是实际开发中最高频遇到的我按症状、原因、解法的方式整理成速查表再展开讲几个重点。症状根因解法模型返回的arguments不是合法JSON大模型偶尔会输出带注释或多余逗号的JSON用修正库或正则兜底最保险是让模型强制输出JSON模式Agent陷入死循环反反复复调同一个工具结果信息不足模型拿不到有效状态限制最大迭代数工具层面返回更明确的结果状态上下文越跑越大API费用飙升工具结果太长全量回填工具结果截断对话历史滑动窗口模型始终不调用某个工具描述含糊、模型不支持函数调用、工具列表过多重写description确认模型型号支持精简工具数量工具执行报错导致整个Agent崩溃缺少全局异常兜底try-except包裹所有工具执行异常转为文本用户请求触发了不该调用的工具权限控制缺失或参数校验不严安全闸门加白名单加入执行前校验5.1 模型输出的JSON一点都不JSONFunction Calling的arguments字段偶尔会出现非法JSON。尤其是模型在生成参数时加入注释、尾逗号、或者干脆把字符串值写错格式JSON解析直接抛异常。空手解析必崩我在解析处做了try-except兜底解析失败就传空字典。但这只是应急手段模型拿着空参数去执行工具工具大概率也会报错不过至少不会整个程序挂掉。要系统性地解决这个问题推荐两条路一是使用支持JSON Mode的模型服务让API强制模型输出合法JSON二是引入一个容错解析层把常见的非法JSON修正后再交个json.loads。社区里有不少现成的修复库核心思路就是清理尾逗号、补齐引号、处理单引号包裹。我后来的做法是上面两路都走能开JSON模式就开开不了就用修正层兜底双保险。5.2 Agent陷进死循环早期版本我没有设max_iterations结果某次测试让模型读一个文件再总结模型读完文件后居然又调用了一次读取工具然后循环往复直到我强制杀掉进程。排查发现模型没有把文件内容已获得这个事实固化下来它还在尝试重复获取。这里有一个值得沉淀的经验工具返回的内容信息量要足够而且要包含状态暗示。我后来在文件读取工具的结果开头加了一行文件读取成功内容共X字符等于给模型一个明确的完成信号。另外最大迭代次数必须硬性配置一旦超出就终止循环并提示模型基于已有信息回答。循环守卫在任何Agent系统里都是保命设置没有它在生产环境跑迟早会被模型模式锁死搞到崩溃。5.3 上下文被工具结果撑爆搜索工具刚上线时我试过让模型搜索一个热门话题返回了二十条结果摘要全量塞回对话。下一轮模型开始失忆连用户最初问什么都答不对了。检查API日志时发现这一轮请求的上下文里工具结果占了八成token。从那以后我就严格执行工具结果摘要化策略超过2000字符必须截断普通场景优先让工具自己整理摘要返回而不是把原始内容抛给模型。另外对话历史我会做一个200条消息的硬窗口超出就从头部裁剪。上下文管理直接关系Agent的稳定性和成本值得多花时间打磨。5.4 工具被安全策略禁用执行权限失控的教训用过浏览器扩展的人都知道Chrome一旦判定扩展包含恶意软件、可疑行为或违反策略会直接禁用这个扩展。我在LLM扩展上也有过同款遭遇某次调试中模型生成了对read_file的调用参数是数据库中取出来的一段未经过滤的用户输入路径直接穿过了白名单目录幸好白名单机制拦住了。这件事让我意识到工具层的安全检查必须在执行前而不是执行后。而且这里有个很隐蔽的坑模型的参数生成过程是不可预测的你没法保证它不输出恶意路径或危险表达式。所以所有工具都要走统一的安全闸门执行前做参数校验、权限判断不满足的请求一律拒绝并返回错误信息。Chrome禁用的是整个扩展我们禁用的则是单次调用颗粒度更细防线必须更严。5.5 扩展装了却没人调为什么模型无视工具清单还有一类高频问题工具明明注册了清单也传给模型了但模型就是不用。排查顺序一般是这样的先确认模型服务本身支持函数调用有些轻量模型压根不支持tool_calls传了工具清单它也只会当没看见再检查工具描述太宽泛的description会让模型不知道该在什么场景下调用最后精简工具数量一次塞太多工具模型容易眼花要么乱选要么干脆不选。另一个比较隐蔽的原因是工具名和描述里的关键词不匹配。用户口语说的是算一下工具描述里写的是执行数学表达式计算模型的语义关联可能搭不上。后来我把用户常用表达和工具描述做了对齐比如描述里加上算、计算、多少、几期这类触发词调用率明显提升。给模型写工具文档和给用户写功能文案一样要理解对方怎么想而不是我有什么。6. 从7.2版本中沉淀的实践笔记这个项目折腾到现在最有价值的已经不是代码本身而是我形成的一套关于如何优雅地让模型动手的方法论。最开始以为自己写的是工具调用后来发现核心是设计一套能让模型稳定理解的协议最开始以为难点在模型能力后来发现难点在边界控制——模型的自由度给多少、安全约束怎么设、工具结果如何可视化每一项都比跑通功能更花时间。最后分享一个后续的扩展方向。如果你准备在7.2这个基础上继续往前走我建议下一步做工具路由当工具数量超过十几个时不要把所有工具一次性塞给模型而是先让模型从一组轻量路由分类器里选出方向再加载对应的细分工具集。这一步做上去整个系统的工具扩展上限会大很多模型也不会因为工具清单太长而频繁决策失误。给还在折腾LLM扩展的朋友一句话先别急着上复杂框架把计算器、搜索、文件读写这三个最朴素的工具跑通把循环的日志打全把安全边界画清楚你对Agent系统的理解会上一个台阶。剩下的都是一点一点给扩展坞加外设的事。