ARTICLE DETAIL

资讯详情

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

大模型Agent从零搭建实战:核心架构、工具调用与避坑指南

大模型Agent从零搭建实战:核心架构、工具调用与避坑指南 1. 大模型Agent到底是什么为什么现在值得学1.1 从一个真实场景说起去年下半年我接手了一个内部工具的需求帮运营团队做一个能自动读取后台数据、生成日报、并且根据异常指标主动发出提醒的小助手。一开始我想得很简单调个大模型接口写个Prompt模板把数据塞进去让它输出不就行了。结果真做起来才发现纯靠Prompt的方案根本撑不住——数据要分页拉取、异常判断需要多轮确认、日报格式要动态调整、有时候还得回头查历史记录。这时候我才意识到我需要的不是一个更聪明的对话框而是一个能自己决定下一步做什么的Agent。大模型Agent这个概念说白了就是让大模型从你问我答升级成你给目标我自己想办法完成。它不再只是生成文本而是能调用工具、拆解任务、记住上下文、根据结果调整策略。你可以把它理解成一个刚入职的实习生脑子好使大模型推理能力但需要给他配电脑工具调用、给他交代清楚目标任务定义、允许他犯错后重试循环控制。这篇文章适合谁看如果你已经用过大模型API写过一些Prompt但总觉得差点意思想让模型真正帮你干活而不是只聊天那这篇就是写给你的。如果你完全没接触过大模型建议先花半天时间了解一下基本的API调用和Prompt写法再回来看这篇会更顺。1.2 Agent和普通大模型调用的本质区别很多人第一次听到Agent会觉得不就是加了个循环吗。这话对了一半。普通的大模型调用是单轮映射输入Prompt输出结果结束。而Agent是一个带状态的决策循环观察当前情况决定用哪个工具执行动作观察结果再决定下一步直到任务完成或达到终止条件。这里有个关键点容易被忽略Agent的核心不是循环而是决策。循环只是形式真正难的是让模型在每一步都做出合理的判断——什么时候该调用工具什么时候该直接回答什么时候该放弃。我见过太多新手写的Agent本质上就是一个while循环里塞了个Prompt模型每轮都在假装思考实际上没有任何有效的决策逻辑。从工程角度看一个最小可用的Agent至少包含四个部分大脑LLM、工具集Tools、记忆Memory、编排逻辑Orchestration。大脑负责推理和决策工具集是它能调用的外部能力记忆让它能记住之前发生了什么编排逻辑控制整个循环的流程和终止条件。这四个部分缺一个Agent就跑不起来。1.3 为什么现在学Agent是个好时机两年前做Agent你得自己搭框架、自己处理工具调用的解析、自己搞定上下文管理光是基础设施就能耗掉大半精力。现在不一样了主流的大模型都原生支持Function Calling各种Agent框架也成熟了你只需要关注业务逻辑本身。更重要的是Agent的应用场景正在从Demo走向生产。我身边做工业AI检测的朋友已经在用Agent做多模态质检——模型看到异常图片后自己决定是调用历史数据库比对还是触发人工复核流程。做金融分析的朋友用Agent自动拉取K线数据、计算指标、生成分析报告。这些场景在两年前还停留在论文里现在已经有人真金白银在跑了。当然热度高也意味着鱼龙混杂。市面上很多所谓的Agent教程其实就是把官方文档翻译了一遍缺少真实的踩坑经验。我写这篇的目的就是把我自己从零搭Agent过程中遇到的实际问题、做过的取舍、踩过的坑尽可能完整地分享出来。2. 动手之前核心概念和架构选型2.1 先把几个容易混淆的概念理清楚在动手写代码之前有几个概念必须先分清楚否则后面选型的时候会一头雾水。Agent和Workflow的区别Workflow是预先定义好的流程每一步做什么是写死的大模型只是其中某个环节的处理器。Agent则是把流程控制权交给模型由模型决定下一步。举个例子如果你明确知道先查数据库再算指标最后生成报告那用Workflow就够了稳定可控。但如果你不确定用户会问什么、需要几步才能完成那就得上Agent。Agent和Chain的区别Chain是把多个LLM调用串起来本质还是线性的。Agent是带分支和循环的模型可以在任意节点决定回到上一步或者走另一条路。Agent和Tool的区别Tool是Agent能调用的一个具体能力比如查询天气、发送邮件。Agent是调度这些Tool的决策者。很多人把写一个Function Calling当成做Agent这是不对的那只是给Agent准备了一个工具而已。Agent和Multi-Agent的区别单个Agent自己干活Multi-Agent是多个Agent分工协作。Multi-Agent听起来很酷但我个人的建议是新手先从单Agent做起。多Agent的通信开销、状态同步、错误传播问题会把你折磨到怀疑人生。等单Agent跑通了确实遇到瓶颈了再考虑拆分。2.2 架构选型ReAct、Plan-and-Execute还是别的目前主流的Agent架构有这么几种我按上手难度和适用场景排个序。ReActReasoning Acting是最经典的架构核心思想是让模型交替进行思考和行动。每一轮模型先输出一段推理Thought然后决定调用哪个工具Action拿到结果后Observation再进入下一轮。这个架构的好处是简单直观调试方便缺点是每一步都要调用一次大模型Token消耗大而且容易陷入循环。Plan-and-Execute是先让模型制定一个完整计划然后按计划逐步执行。好处是全局视野好Token消耗相对少。缺点是计划一旦制定就不好改遇到意外情况容易卡死。我试过用这个架构做一个数据清洗Agent结果遇到脏数据格式不一致计划里没考虑这种情况整个流程就崩了。Reflection是在ReAct基础上加了自我反思环节模型执行完一步后会评估结果质量不好就重来。这个架构适合对结果质量要求高的场景但延迟会明显增加。我的建议是新手从ReAct开始把整个循环跑通理解每一步在干什么。等熟悉了再根据具体场景选择或组合其他架构。别一上来就追求最先进的架构能跑通的简单架构比跑不通的复杂架构有价值得多。2.3 工具选型框架还是手写这是新手最容易纠结的问题。我的答案是先手写一遍再用框架。手写一个最小Agent大概只需要一百多行代码。你需要自己实现Prompt模板、工具调用的解析、循环控制、上下文管理。这个过程会让你真正理解Agent的每个环节在干什么。我见过太多人直接用框架结果出了问题完全不知道从哪查起因为框架把太多细节封装了。手写跑通之后再根据需求选框架。如果你做的是通用场景LangChain、LlamaIndex这些生态成熟的框架能省很多事。如果你对性能和控制力要求高可以考虑更轻量的方案或者基于框架做二次开发。这里要提醒一点框架不是银弹。我见过一个团队用某框架做Agent结果发现框架的默认上下文管理策略会把历史消息全部塞进去Token消耗爆炸。最后他们不得不重写整个上下文管理模块。所以选框架之前一定要搞清楚它的默认行为是什么。2.4 大模型选型不是越贵越好Agent对模型的要求和普通对话不一样。普通对话可能只需要模型会说话但Agent需要模型会推理、会调用工具、会遵循格式。我实测下来选模型主要看三个维度Function Calling能力、推理能力、成本。Function Calling能力是硬门槛模型如果连工具调用格式都输出不对后面全白搭。推理能力决定了Agent能不能处理复杂任务。成本则直接决定了你的Agent能不能规模化。具体选哪个模型我不做具体推荐因为模型迭代太快了。但有个原则先用能力最强的模型把流程跑通再考虑用更便宜的模型替换。很多人一上来就用便宜模型结果各种格式错误、推理失败浪费大量时间在调试上最后算下来还不如直接用强模型。另外如果你的场景对数据隐私要求高可以考虑本地部署。现在本地部署的门槛已经低很多了消费级显卡就能跑一些中等规模的模型。但要注意本地模型的Function Calling能力通常比云端模型弱需要做更多的格式约束和容错处理。3. 从零搭建第一个Agent完整实操3.1 环境准备和最小依赖我假设你已经有了Python基础并且能调用至少一个大模型的API。下面是我常用的最小依赖清单。pip install openai pip install pydantic pip install requests就这三个不需要更多。很多人一上来就装一堆框架结果环境冲突搞半天。我建议先用最少的依赖把核心逻辑跑通。如果你要用本地模型还需要装对应的推理框架。但为了降低上手难度这篇先用云端API演示本地部署的差异我会在后面的章节单独说。3.2 定义工具Agent的手和脚工具就是Agent能调用的函数。定义工具的关键是描述要清晰因为模型是根据描述来决定用哪个工具的。import json import requests def get_weather(city: str) - str: 查询指定城市的当前天气。 Args: city: 城市名称例如北京、上海 Returns: 天气信息的字符串描述 # 这里用模拟数据实际项目替换成真实API weather_data { 北京: 晴气温25度湿度40%, 上海: 多云气温28度湿度65%, 广州: 小雨气温30度湿度80% } return weather_data.get(city, f未找到{city}的天气数据) def calculate(expression: str) - str: 计算数学表达式。 Args: expression: 数学表达式字符串例如2 3 * 4 Returns: 计算结果 try: result eval(expression) return str(result) except Exception as e: return f计算错误: {e}注意看这两个函数的docstring它们不是写给人看的是写给模型看的。模型会根据这些描述判断什么时候该调用哪个工具。我踩过的坑是一开始docstring写得很随意结果模型经常调错工具。后来把描述写详细包括参数格式、返回值含义调用准确率明显提升。工具定义好之后需要转成模型能识别的格式tools [ { type: function, function: { name: get_weather, description: 查询指定城市的当前天气, parameters: { type: object, properties: { city: { type: string, description: 城市名称例如北京、上海 } }, required: [city] } } }, { type: function, function: { name: calculate, description: 计算数学表达式, parameters: { type: object, properties: { expression: { type: string, description: 数学表达式例如 2 3 * 4 } }, required: [expression] } } } ]3.3 核心循环Agent的心脏这是整个Agent最核心的部分也是最容易出问题的地方。import openai client openai.OpenAI(api_key你的API Key) def run_agent(user_input: str, max_turns: int 10): messages [ {role: system, content: 你是一个助手可以使用工具来帮助用户解决问题。请一步步思考必要时调用工具。}, {role: user, content: user_input} ] for turn in range(max_turns): response client.chat.completions.create( modelgpt-4, messagesmessages, toolstools, tool_choiceauto ) message response.choices[0].message # 如果模型没有调用工具说明它认为可以直接回答 if not message.tool_calls: return message.content # 把模型的回复加入历史 messages.append(message) # 执行所有工具调用 for tool_call in message.tool_calls: function_name tool_call.function.name function_args json.loads(tool_call.function.arguments) # 根据函数名调用对应的函数 if function_name get_weather: result get_weather(**function_args) elif function_name calculate: result calculate(**function_args) else: result f未知工具: {function_name} # 把工具结果加入历史 messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) return 达到最大轮次限制任务未完成这段代码看起来简单但有几个关键点必须注意。第一max_turns是必须的。没有这个限制模型可能陷入无限循环一直调用工具不给出最终答案。我一开始没加这个限制结果有一次Agent卡在查询天气-发现数据不对-重新查询的循环里烧了不少Token。第二工具执行结果必须加回messages。很多人忘了这一步导致模型不知道工具执行的结果下一轮又重复调用同一个工具。第三tool_call_id必须对应。每个工具调用都有唯一的id返回结果时必须带上对应的id否则模型无法把结果和调用对应起来。3.4 跑起来看看效果result run_agent(北京今天天气怎么样如果温度超过20度帮我算一下25乘以4等于多少) print(result)执行过程大概是这样模型先调用get_weather查询北京天气拿到晴气温25度的结果后判断温度超过20度于是调用calculate计算25乘以4最后综合两个结果给出回答。这个例子虽然简单但已经包含了Agent的核心要素多步推理、工具调用、结果整合。你可以把工具换成真实的API把任务换成更复杂的场景整个框架是不变的。3.5 上下文管理别让历史消息撑爆Token上面的代码有个隐患messages会随着轮次增加越来越长最后可能超出模型的上下文窗口。我实测过一个复杂任务跑了十几轮之后光历史消息就占了上万Token。解决方案有几种。最简单的是滑动窗口只保留最近N轮的消息。但这样会丢失早期的重要信息。更好的做法是摘要压缩把早期的对话总结成一段简短的描述保留关键信息。def compress_messages(messages, keep_recent4): 压缩历史消息保留最近几轮早期的做摘要 if len(messages) keep_recent 2: # 2是system和user return messages system_msg messages[0] recent_msgs messages[-keep_recent:] old_msgs messages[1:-keep_recent] # 把旧消息总结成一段话 summary 之前的对话摘要 for msg in old_msgs: if msg.get(role) assistant and msg.get(content): summary msg[content][:100] ... return [system_msg, {role: system, content: summary}] recent_msgs这个压缩策略不是最优的但足够应付大部分场景。更复杂的方案可以用模型来做摘要但会增加额外的调用成本。4. 进阶实战让Agent真正能干活4.1 记忆系统让Agent记住该记的基础版的Agent只有短期记忆当前对话的messages。但真实场景往往需要长期记忆——比如记住用户的偏好、记住之前处理过的类似任务、记住失败过的方案。记忆系统通常分三层短期记忆当前对话、工作记忆当前任务的相关信息、长期记忆跨会话的知识。短期记忆就是messages工作记忆可以用一个结构化的字典来存长期记忆则需要向量数据库。class AgentMemory: def __init__(self): self.short_term [] # 当前对话 self.working {} # 当前任务的关键信息 self.long_term [] # 长期记忆实际项目用向量库 def add_working(self, key, value): self.working[key] value def get_working(self, key): return self.working.get(key) def add_long_term(self, content): self.long_term.append(content) def search_long_term(self, query, top_k3): # 简化版实际用向量相似度搜索 return self.long_term[-top_k:]我踩过的坑是一开始把所有信息都往长期记忆里塞结果检索出来的都是无关内容。后来改成只存任务完成后的总结和用户明确表达的偏好检索质量明显提升。4.2 错误处理Agent崩了怎么办Agent在生产环境跑出错是常态。工具调用失败、模型输出格式错误、超时、达到轮次限制这些都得处理。我的经验是分三层处理工具层重试、Agent层降级、系统层兜底。工具层重试很简单调用失败后重试2-3次每次间隔递增。Agent层降级是指当Agent无法完成任务时返回一个预设的兜底回答而不是直接报错。系统层兜底是记录所有失败案例定期分析优化。def safe_tool_call(func, args, max_retries3): for i in range(max_retries): try: return func(**args) except Exception as e: if i max_retries - 1: return f工具调用失败: {e} time.sleep(2 ** i) # 指数退避还有一个容易被忽略的问题模型输出的工具参数可能不符合格式。比如要求传JSON模型传了个Python字典字符串。这时候需要做容错解析。def parse_tool_args(args_str): try: return json.loads(args_str) except json.JSONDecodeError: # 尝试修复常见的格式问题 args_str args_str.replace(, ) try: return json.loads(args_str) except: return {}4.3 并发处理Agent怎么扛住高并发这是很多人关心的问题。Agent的并发瓶颈通常不在模型调用而在工具执行和状态管理。如果工具是IO密集型的比如调API、查数据库可以用异步来提升并发。如果工具是CPU密集型的比如图像处理那就得用多进程或者任务队列。状态管理是另一个难点。每个用户的Agent会话必须隔离不能串。我的做法是每个会话一个独立的Memory实例用session_id来区分。from concurrent.futures import ThreadPoolExecutor class AgentPool: def __init__(self, max_workers10): self.executor ThreadPoolExecutor(max_workersmax_workers) self.sessions {} def run(self, session_id, user_input): if session_id not in self.sessions: self.sessions[session_id] AgentMemory() future self.executor.submit( run_agent_with_memory, user_input, self.sessions[session_id] ) return future实测下来单机跑10-20个并发Agent是没问题的瓶颈主要在模型API的速率限制。如果要支撑更高并发需要考虑请求队列和限流。4.4 多模态Agent让Agent能看图现在很多场景需要Agent处理图片比如工业质检、文档识别。多模态Agent的核心是把图片作为输入的一部分传给模型。def run_multimodal_agent(image_path, user_input): import base64 with open(image_path, rb) as f: image_data base64.b64encode(f.read()).decode() messages [ { role: user, content: [ {type: text, text: user_input}, { type: image_url, image_url: {url: fdata:image/jpeg;base64,{image_data}} } ] } ] response client.chat.completions.create( modelgpt-4-vision-preview, messagesmessages, toolstools ) return response多模态Agent的坑在于图片会占用大量Token。一张高清图片可能占上千Token如果Agent需要多轮处理图片成本会很高。我的做法是先把图片压缩到合理尺寸或者先用专门的视觉模型提取关键信息再把文本信息传给Agent。5. 常见问题排查与避坑指南5.1 模型不调用工具怎么办这是新手遇到最多的问题。模型明明有工具可用却直接回答不调用工具。原因通常有三个工具描述不清楚、系统Prompt没引导、模型能力不够。排查顺序先看工具描述是不是太模糊了比如处理数据这种描述模型根本不知道什么时候该用。改成查询指定城市的当前天气就清楚多了。再看系统Prompt有没有明确告诉模型你可以使用工具。最后看模型有些小模型的Function Calling能力确实弱换个强模型试试。5.2 Agent陷入死循环怎么破死循环的表现是Agent反复调用同一个工具或者在不同工具之间来回跳就是不给最终答案。解决方案设置最大轮次、检测重复调用、强制终止。def detect_loop(messages, window4): 检测最近几轮是否在重复调用同一个工具 recent_tools [] for msg in messages[-window:]: if msg.get(role) assistant and msg.get(tool_calls): for tc in msg[tool_calls]: recent_tools.append(tc.function.name) if len(recent_tools) 3 and len(set(recent_tools[-3:])) 1: return True return False检测到循环后可以强制让模型给出最终答案或者直接返回兜底回答。5.3 工具调用参数错误怎么处理模型输出的参数格式不对是常见问题。比如要求传整数模型传了字符串要求传JSON模型传了自然语言。处理策略在工具函数里做类型转换和容错。def robust_get_weather(city): if not isinstance(city, str): city str(city) city city.strip().replace(市, ) return get_weather(city)另外在工具描述里明确参数格式也能减少这类错误。比如city参数必须是字符串不要带市字。5.4 常见问题速查表问题现象可能原因排查方向解决方案模型不调用工具工具描述模糊检查docstring补充详细描述和示例Agent死循环缺少轮次限制检查循环控制加max_turns和循环检测参数格式错误模型输出不稳定检查工具参数定义加容错解析和类型转换Token消耗过大上下文未压缩检查messages长度加滑动窗口或摘要压缩工具执行超时外部API慢检查工具实现加超时和重试机制多轮后答非所问上下文污染检查历史消息清理无关消息加摘要并发时状态串了会话未隔离检查session管理每个会话独立Memory模型输出格式错模型能力不足换模型测试用Function Calling强的模型5.5 几个我踩过的坑坑一把System Prompt写得太长。我一开始觉得System Prompt越详细越好结果写了两千多字模型反而抓不住重点。后来精简到三百字以内只保留最关键的指令效果反而更好。坑二工具粒度太细。我一开始把每个小操作都做成一个工具结果模型在十几个工具之间选择经常选错。后来合并成几个粗粒度的工具每个工具内部处理多个步骤准确率明显提升。坑三忽略Token成本。Agent的Token消耗是普通对话的好几倍因为每轮都要把完整历史传进去。我有个项目没注意这个月底账单出来吓了一跳。后来加了上下文压缩和缓存成本降了六成。坑四没有日志。Agent出问题时如果没有详细的日志根本不知道是哪一步错了。后来我加了完整的日志记录包括每轮的输入输出、工具调用、耗时排查效率提升很多。6. 从Demo到生产还需要补哪些课6.1 评估体系怎么知道Agent好不好Demo阶段靠感觉生产阶段靠数据。你需要一套评估体系来衡量Agent的表现。我常用的指标有任务完成率、平均轮次、工具调用准确率、平均耗时、Token消耗。任务完成率是最核心的但需要人工标注或者用另一个模型来评判。平均轮次反映了Agent的效率轮次太多说明决策不够果断。工具调用准确率可以通过日志统计。class AgentEvaluator: def __init__(self): self.stats { total: 0, success: 0, total_turns: 0, total_tokens: 0 } def record(self, success, turns, tokens): self.stats[total] 1 if success: self.stats[success] 1 self.stats[total_turns] turns self.stats[total_tokens] tokens def report(self): s self.stats return { 完成率: s[success] / s[total], 平均轮次: s[total_turns] / s[total], 平均Token: s[total_tokens] / s[total] }6.2 安全边界Agent不能干什么Agent能调用工具意味着它有行动能力。这既是优势也是风险。必须设置明确的安全边界。我的做法是工具分级、权限控制、敏感操作二次确认。工具分三级只读工具查询类可以直接调用写入工具修改数据需要记录日志危险工具删除、发送必须二次确认。TOOL_PERMISSIONS { get_weather: read, calculate: read, send_email: dangerous, delete_record: dangerous } def check_permission(tool_name, user_role): level TOOL_PERMISSIONS.get(tool_name, read) if level dangerous and user_role ! admin: return False return True6.3 持续优化Agent不是一次性的Agent上线只是开始后面需要持续优化。优化的方向主要有Prompt调优、工具优化、模型替换、架构调整。我的习惯是每周看一次失败案例分析失败原因归类后针对性优化。常见的失败模式有工具选择错误、参数提取错误、多步推理断裂、上下文丢失。每种模式对应不同的优化策略。另外模型迭代很快每隔一段时间就应该重新评估一下当前用的模型是不是最优选择。我一般每季度做一次模型对比测试用同一批测试用例跑不同模型看完成率和成本的变化。6.4 本地部署的注意事项如果你的场景需要本地部署有几个点要特别注意。第一Function Calling能力。本地模型的工具调用能力普遍弱于云端模型需要做更多的格式约束。我的做法是在Prompt里给出严格的输出格式示例并且在解析时做多重容错。第二推理速度。本地模型的推理速度取决于硬件消费级显卡跑中等模型大概每秒几十个Token。如果Agent需要多轮调用总耗时可能达到几十秒用户体验会受影响。可以考虑用量化模型加速或者用更小的模型。第三显存管理。本地部署最容易遇到显存不足的问题。我的经验是留出至少20%的显存余量否则并发请求一上来就容易OOM。第四模型更新。本地模型更新需要重新下载和部署不像云端API那样无缝切换。建议把模型路径做成配置项方便切换。6.5 一些实用的工程建议最后分享几个我在实际项目中总结的工程建议。配置和代码分离。模型名称、API地址、超时时间、最大轮次这些参数全部放到配置文件里不要硬编码。这样换模型、调参数的时候不用改代码。日志要结构化。用JSON格式记录日志方便后续分析和检索。每条日志至少包含时间戳、session_id、轮次、输入、输出、工具调用、耗时、Token数。测试用例要积累。每次遇到失败的案例都把它加进测试集。时间长了你就有了一个覆盖各种边界情况的测试集每次改动后跑一遍能快速发现回归问题。版本管理要严格。Prompt、工具定义、模型配置这些都要纳入版本管理。Agent的行为对Prompt非常敏感改一个字可能效果就变了。没有版本管理出了问题根本回滚不了。监控要实时。生产环境的Agent必须有实时监控包括成功率、平均耗时、Token消耗、错误率。设置告警阈值异常时及时通知。我在实际项目里最大的体会是Agent开发最难的不是写代码而是定义清楚任务边界。很多失败案例根源在于任务本身就没定义清楚模型再强也做不好。所以在动手之前先花时间把这个Agent到底要解决什么问题、成功的标准是什么、边界在哪里想清楚比急着写代码重要得多。
返回列表