ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从零手写AI代理:核心架构、工具调用与循环控制实战

从零手写AI代理:核心架构、工具调用与循环控制实战 1. 从零构建智能AI代理的整体设计思路1.1 为什么现在值得动手做AI代理AI代理这个词最近一年被聊得很多但真正动手从零搭一个的人其实没想象中那么多。大部分人停留在调用一个对话接口、套个界面就结束的阶段。而所谓Agentic AI核心区别在于它不只是被动回答问题而是能自己拆解任务、调用工具、观察结果、再决定下一步动作形成一个闭环。你可以把它理解成一个会自己干活的数字助手而不是一个只会聊天的问答机器。我最初接触这个方向是因为手头有一堆重复性的信息整理工作需要从不同来源抓取内容、做摘要、分类归档、再生成一份日报。纯靠手动做每天要花一个多小时。用普通脚本写死流程又太脆稍微换个数据格式就崩。于是我开始尝试用AI代理的思路来重构让模型自己判断该调用哪个工具、该走哪条分支。实测下来这套东西一旦跑通稳定性远超预期而且扩展新能力只需要加一个工具函数不用重写整个流程。这篇文章适合谁看如果你有基本的编程概念能看懂Python代码知道什么是API调用那就可以跟着走。不需要你懂深度学习不需要你训练模型我们要做的是用现成的模型能力加上自己的工程逻辑搭出一个能实际干活的代理系统。整个过程我会把设计取舍、参数计算、踩过的坑都摊开讲你可以直接抄作业也可以按自己的场景改。1.2 代理系统的核心架构拆解一个能用的AI代理拆开来看其实就四块大脑、工具、记忆、循环控制。大脑负责推理和决策通常就是一个大语言模型工具是代理能调用的外部能力比如搜索、计算、读写文件、调接口记忆分短期和长期短期存当前任务的上下文长期存跨会话的知识循环控制则是那个让代理反复“思考-行动-观察”直到任务完成的调度器。为什么这么分因为这样每一块都可以独立替换和调试。比如你觉得模型不够聪明换一个更强的就行工具和记忆不用动。你觉得工具太少加一个函数注册进去就行大脑不用改。这种解耦设计在实际维护中太重要了我早期把所有逻辑揉在一个大函数里后来想加个新工具改得痛不欲生重构之后才体会到模块化的好处。这里有个关键取舍是用现成的代理框架还是从零手写我的建议是学习阶段一定从零手写。框架帮你封装了太多细节你调不通的时候根本不知道问题出在哪。手写一遍之后你再去看框架的源码会有一种“原来就是这么回事”的通透感。而且手写的核心循环其实不到两百行代码完全在可控范围内。1.3 技术选型与依赖说明模型这块我建议用支持函数调用能力的接口。现在主流的大模型服务基本都支持工具调用你选一个自己方便接入的就行。本地模型也可以但要注意本地小模型在复杂推理和工具调用格式遵循上会弱一些适合简单任务或者对隐私要求高的场景。热词里提到的“本地模型”确实是个趋势我后面会专门讲怎么接。编程语言用Python生态最全调试也方便。核心依赖就几个一个HTTP请求库用来调模型接口一个JSON处理库Python自带就够再加一些标准库做文件操作和时间处理。不需要装一堆重型框架越轻越好这样你能看清每一行在干什么。环境准备上我建议单独建一个虚拟环境把依赖固定住。因为代理系统经常需要反复调试依赖版本一变行为可能就不一样。用requirements.txt把版本锁死换机器也能一键复现。这个习惯在后期做多代理协作时尤其重要不然环境问题能吃掉你一半的调试时间。2. 核心细节解析与实操要点2.1 代理循环的工作原理与实现要点代理循环是整个系统的心脏。它的基本逻辑是把用户任务和当前上下文发给模型模型返回一个决策要么是直接给出最终答案要么是要求调用某个工具。如果是调用工具系统就执行工具、拿到结果、把结果追加到上下文里然后再发给模型如此往复直到模型给出最终答案或者达到最大轮次限制。这个循环里最容易被忽视的是终止条件。我见过太多人写的代理跑着跑着就停不下来要么是模型一直觉得任务没完成要么是工具返回的结果让它陷入死循环。所以你必须设一个最大迭代次数比如10轮或15轮超过就强制停止并返回当前状态。这个数字怎么定看任务复杂度。简单问答3到5轮足够多步骤的信息整理任务可能需要8到12轮。我一般默认设10然后根据实际日志调整。另一个要点是上下文的组织方式。每一轮的工具调用和结果都要按特定格式追加到消息列表里模型才能正确理解发生了什么。通常用角色区分用户消息、助手消息包含工具调用请求、工具消息包含执行结果。格式一定要严格少一个字段模型就可能解析失败。我建议在代码里写一个专门的函数来构造这些消息不要手动拼字符串容易出错。注意工具调用的参数格式必须和模型约定的schema完全一致类型不对、字段名拼错、必填项缺失都会导致调用失败。建议在工具注册时就做好参数校验把错误拦在调用之前。2.2 工具设计与注册机制工具是代理的手和脚。一个设计良好的工具应该满足几个条件功能单一、参数明确、返回结构清晰、有错误处理。我见过有人把“搜索并总结并保存”做成一个工具结果模型根本不知道怎么传参因为参数太多了。正确做法是拆成三个工具搜索、总结、保存让模型自己编排顺序。工具注册机制我推荐用装饰器加字典的方式。定义一个工具注册表每个工具函数用装饰器标记自动把函数名、描述、参数schema注册进去。这样加新工具只需要写一个函数加一个装饰器不用改调度逻辑。描述字段特别重要模型就是靠这个描述来决定什么时候调用哪个工具的。描述要写清楚这个工具做什么、什么时候用、参数是什么意思。我一般会写得比较啰嗦因为模型理解得越准调用就越靠谱。参数schema用JSON Schema格式定义每个参数的类型、描述、是否必填。这里有个坑模型有时候会传字符串类型的数字比如把5传给期望整数的参数。所以工具函数内部要做类型转换和校验不能直接信任模型传来的值。我一般会在工具入口加一层参数清洗把常见类型问题处理掉。2.3 记忆系统的分层设计记忆分两层来做。短期记忆就是当前对话的消息列表随着循环不断增长。但消息列表不能无限增长否则会超出模型的上下文窗口而且成本也会飙升。所以需要做上下文压缩当消息数量超过阈值时把早期的工具调用结果做摘要只保留关键信息。摘要可以用模型来做也可以用规则提取看你对信息完整度的要求。长期记忆则是跨会话的。比如代理今天处理了一批数据明天再处理时能记得之前的分类规则。实现方式可以简单到写一个JSON文件复杂到接一个向量数据库。我的建议是先用文件系统把重要结论和偏好存成结构化数据需要时读进来拼到系统提示里。等数据量大了再考虑向量检索不要一上来就上重型方案。系统提示是记忆系统里最稳定的部分它定义了代理的角色、行为准则、可用工具列表。这个提示要写得清晰、具体不要含糊。比如“你是一个助手”就不如“你是一个信息整理代理负责从网页内容中提取要点并分类遇到不确定的分类时先询问用户”。越具体代理行为越可控。2.4 错误处理与重试策略代理系统比普通程序更容易出错因为它依赖模型输出而模型输出是不确定的。常见的错误有模型返回格式不对、工具调用参数错误、工具执行超时、模型陷入循环。每一种都要有对应的处理策略。格式错误就重试把错误信息追加到上下文里让模型重新生成。参数错误也类似把校验失败的原因告诉模型让它修正。工具超时要设超时时间超时后返回一个明确的错误信息让模型决定是重试还是换方案。陷入循环则靠最大轮次限制来兜底。重试次数不要太多一般两次就够了。重试太多次不仅浪费成本还可能让模型在错误的方向上越走越远。我一般会在重试两次后直接返回错误让用户介入。这个策略在实际使用中比较平衡既给了模型自我修正的机会又不会无限拖延。3. 实操过程与核心环节实现3.1 环境搭建与基础依赖安装先建虚拟环境这是好习惯。用python -m venv agent_env创建然后激活。激活命令在Windows和macOS/Linux上不一样Windows是agent_env\Scripts\activatemacOS和Linux是source agent_env/bin/activate。激活后命令行前面会出现环境名说明成功了。然后装依赖。核心就两个requests用来发HTTP请求python-dotenv用来管理密钥。密钥不要硬编码在代码里放到.env文件里用dotenv读进来。这是基本的安全习惯也方便切换不同环境。.env文件要加到.gitignore里别提交到代码仓库。pip install requests python-dotenv目录结构我建议这样组织一个agent.py放主循环一个tools.py放工具函数一个config.py放配置一个.env放密钥。这样结构清晰后期扩展也方便。不要把所有东西塞一个文件里超过五百行之后你会后悔的。3.2 模型接口封装与调用参数计算封装一个模型调用函数输入是消息列表和工具定义输出是模型的响应。这里的关键是参数设置。温度参数控制输出的随机性代理场景我建议设低一点0.1到0.3之间因为我们需要模型稳定地遵循格式和逻辑不需要它发挥创意。太高了容易乱来格式错误率会上升。最大输出长度要设够因为工具调用的JSON可能比较长。一般设2000到4000个token比较稳妥。如果设太小模型输出被截断JSON就不完整解析会失败。这个坑我踩过调了半天以为是格式问题结果是长度不够。超时时间设30到60秒。模型推理有时候会慢尤其是复杂任务。设太短会频繁超时设太长又影响体验。我一般设45秒配合重试机制基本能覆盖大部分情况。import os import json import requests from dotenv import load_dotenv load_dotenv() def call_model(messages, toolsNone, temperature0.2, max_tokens3000): url os.getenv(MODEL_API_URL) headers { Authorization: fBearer {os.getenv(MODEL_API_KEY)}, Content-Type: application/json } payload { model: os.getenv(MODEL_NAME), messages: messages, temperature: temperature, max_tokens: max_tokens } if tools: payload[tools] tools resp requests.post(url, headersheaders, jsonpayload, timeout45) resp.raise_for_status() return resp.json()这段代码是基础版实际用的时候要加错误处理和重试。raise_for_status会把HTTP错误抛出来外层捕获后决定是否重试。重试时可以用指数退避第一次等1秒第二次等2秒避免瞬间打爆接口。3.3 工具函数的完整实现示例我拿三个典型工具来演示计算器、读文件、写文件。计算器用来做数学运算读文件用来获取本地数据写文件用来保存结果。这三个工具覆盖了大部分基础场景你可以照着这个模式加自己的工具。计算器工具要注意安全不要直接用eval那太危险了。用ast模块解析表达式只允许基本的数学运算。这样既安全又够用。读文件工具要限制路径防止读到不该读的文件。写文件工具要检查目录是否存在不存在就创建。import ast import operator import os TOOLS {} def register_tool(name, description, parameters): def decorator(func): TOOLS[name] { function: func, schema: { type: function, function: { name: name, description: description, parameters: parameters } } } return func return decorator register_tool( namecalculate, description执行数学计算支持加减乘除和括号, parameters{ type: object, properties: { expression: { type: string, description: 数学表达式如 (35)*2 } }, required: [expression] } ) def calculate(expression): allowed_ops { ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul, ast.Div: operator.truediv, ast.Pow: operator.pow, ast.USub: operator.neg } def eval_node(node): if isinstance(node, ast.Num): return node.n elif isinstance(node, ast.BinOp): return allowed_ops[type(node.op)](eval_node(node.left), eval_node(node.right)) elif isinstance(node, ast.UnaryOp): return allowed_ops[type(node.op)](eval_node(node.operand)) else: raise ValueError(不支持的表达式) tree ast.parse(expression, modeeval) return eval_node(tree.body)这个计算器实现只允许数字和基本运算符其他一律拒绝。实测下来模型生成的表达式基本都在这个范围内够用。如果模型传了不支持的表达式会抛异常外层捕获后把错误信息返回给模型它会自己修正。读文件和写文件工具类似注册进去就行。读文件返回内容字符串写文件返回成功提示。注意写文件时要处理编码统一用UTF-8避免中文乱码。3.4 主循环的完整实现与调试主循环把前面所有部分串起来。逻辑是初始化消息列表把系统提示和用户任务放进去然后进入循环。每轮调用模型检查返回里有没有工具调用。有就执行工具、追加结果、继续循环没有就说明模型给出了最终答案退出循环返回结果。def run_agent(user_task, max_iterations10): messages [ {role: system, content: 你是一个智能代理可以调用工具完成任务。请逐步思考必要时调用工具。}, {role: user, content: user_task} ] tool_schemas [t[schema] for t in TOOLS.values()] for i in range(max_iterations): response call_model(messages, toolstool_schemas) choice response[choices][0] message choice[message] messages.append(message) if not message.get(tool_calls): return message.get(content, ) for tool_call in message[tool_calls]: func_name tool_call[function][name] args json.loads(tool_call[function][arguments]) if func_name in TOOLS: try: result TOOLS[func_name][function](**args) except Exception as e: result f工具执行错误: {str(e)} else: result f未知工具: {func_name} messages.append({ role: tool, tool_call_id: tool_call[id], content: str(result) }) return 达到最大迭代次数任务未完成这段代码是核心骨架实际跑的时候你会发现几个问题。第一消息列表会越来越长需要压缩。第二模型有时候会一次调用多个工具要按顺序执行。第三工具返回的结果太长会撑爆上下文需要截断。这些问题我在下一节详细讲怎么处理。调试的时候把每一轮的消息列表打印出来看模型到底收到了什么、返回了什么。这是最有效的调试手段。我一般会在关键位置加日志记录轮次、工具名、参数、结果。跑几次之后你就能看出模型的决策模式知道哪里需要优化提示词。4. 常见问题与排查技巧实录4.1 模型不调用工具或调用错误工具这是最常见的问题。模型要么直接回答不调工具要么调了不该调的工具。原因通常是工具描述不够清晰或者系统提示没有强调工具的使用场景。解决办法是把工具描述写得更具体在系统提示里明确说“遇到数学计算必须调用calculate工具”。还有一种情况是模型调用了工具但参数不对。比如该传数字传了字符串该传数组传了单个值。这需要在工具函数里做容错同时把参数schema写得更严格加上类型约束和示例。我一般会在描述里加一个例子比如“expression参数示例(35)*2”模型看到例子后传参准确率会明显提升。如果模型反复调错可以在系统提示里加一条规则“调用工具前先确认参数类型和格式”。有时候模型就是需要被提醒一下。另外温度参数调低也有帮助0.1比0.3更稳定。4.2 上下文超长导致调用失败消息列表增长太快是代理系统的通病。每一轮的工具调用和结果都追加进去几轮下来就超了模型的上下文窗口。解决办法是压缩历史消息。我的做法是保留最近N轮完整消息更早的做摘要。摘要用模型生成把关键结论提取出来压缩成一段话。具体实现上可以设一个token阈值比如8000。超过就把最早的一批消息拿出来让模型总结成一段“历史摘要”然后用这段摘要替换掉那批消息。摘要要保留任务目标、已完成步骤、关键结果丢掉中间的冗余细节。这样既控制了长度又不丢失关键信息。注意压缩后的摘要要放在消息列表靠前的位置通常紧跟在系统提示后面。这样模型能先看到历史背景再看当前进展逻辑更顺。另一个技巧是限制工具返回内容的长度。比如读文件工具如果文件很大不要全量返回只返回前2000个字符加一个“内容已截断”的提示。模型需要更多内容时会再调一次工具指定读取范围。这样按需加载避免一次性撑爆上下文。4.3 代理陷入循环或提前终止陷入循环的表现是模型反复调用同一个工具、传同样的参数、得到同样的结果但就是不结束。这通常是因为任务目标不明确或者工具返回的结果让模型误以为没完成。解决办法是设最大轮次限制同时在系统提示里加一条“如果连续两次得到相同结果请换一种方法或直接给出当前最佳答案”。提前终止则是模型觉得任务完成了但实际上没完成。这往往是任务描述太模糊模型理解偏了。解决办法是把任务拆得更细在系统提示里明确列出完成标准。比如“任务完成的标志是生成一份包含三个要点的总结”模型有了明确目标就不容易提前收工。还有一种情况是模型在等待用户输入但代理系统没有交互界面。这需要在系统提示里说明“不要询问用户直接基于现有信息做出最佳决策”。代理系统最好是全自动的需要交互的场景应该由外层程序处理而不是让模型在循环里卡住。4.4 常见问题速查表问题现象可能原因排查方法解决策略模型不调工具工具描述不清、系统提示未强调打印工具schema和系统提示补充描述和示例明确使用场景参数格式错误schema不严格、模型理解偏差记录模型传入的原始参数加类型校验和容错描述里加示例上下文超长消息列表无限增长统计每轮token数压缩历史消息截断工具返回陷入循环任务目标模糊、结果无变化查看连续几轮的工具调用设最大轮次提示换方法提前终止完成标准不明确检查最终输出是否满足需求细化任务描述明确完成标志工具执行超时外部接口慢、文件太大记录工具执行耗时设超时异步处理分块读取模型输出截断max_tokens太小检查响应finish_reason增大max_tokens到4000中文乱码编码不一致检查文件读写编码统一用UTF-8这张表是我实际调试中积累的基本覆盖了八成以上的问题。遇到新问题先对照这张表排查大部分都能快速定位。剩下两成通常是模型本身的限制换更强的模型或者简化任务就能解决。4.5 本地模型接入的注意事项热词里提到本地模型我单独说一下。本地模型的优势是数据不出本地适合处理敏感信息。但劣势也明显推理能力弱、工具调用格式遵循差、速度慢。如果你要用本地模型跑代理建议选参数量大一些的至少7B以上太小了基本没法用。接入方式和云端接口类似只是URL指向本地服务。但要注意本地模型的输出格式可能不完全遵循标准需要做额外的解析和容错。我一般会在调用后加一层格式清洗把常见的格式问题修掉。另外本地模型的速度是瓶颈超时时间要设长一些或者用流式输出改善体验。实际测试下来本地模型适合简单任务比如单轮工具调用、格式固定的信息提取。复杂多步推理还是得用云端强模型。一个折中方案是混合使用简单任务走本地复杂任务走云端在调度层做路由。这样兼顾成本和隐私是个实用的架构。4.6 实操心得与避坑建议最后分享几条我踩坑换来的经验。第一日志一定要详细每一轮的输入输出都记下来出问题的时候能快速回溯。我早期日志打得太少调一个问题要跑好几遍才能定位效率极低。第二工具函数要幂等同一个调用执行多次结果应该一样这样重试才安全。第三系统提示要版本管理改了什么要记下来不然过几天自己都忘了为什么这么写。还有一点不要追求一次做到完美。代理系统是迭代出来的先跑通最简单的单工具场景再逐步加工具、加记忆、加压缩。每加一个功能就测一遍确保没破坏原有逻辑。我见过有人一上来就搭多代理协作结果基础循环都没调通白白浪费时间。从零开始一步一步来反而最快。
返回列表