
去年我从头到尾把一个纯粹的服务端爬虫项目翻新成了带大模型Agent的自动化数据系统才真正理解“大模型开发”和“大模型Agent开发”是两码事。如果你只是想调API聊聊天那教程到处都是但你要让模型自己决定下一步干什么、去调用哪个工具、出错以后自己修正这就是Agent开发的活。这篇博文不打算从“什么是大模型”这种概念讲起直接给你一条从零入门的实操路径先拆清楚Agent的底子再带你手写一个能跑的ReAct循环最后聊清楚工具调用、记忆、规划和那些文档里不会写的坑。我尽量不写空话所有代码都是跑过的你照着做就能看到效果。1. 先搞清楚Agent到底是个什么东西1.1 Agent不是ChatGPT套层壳很多人觉得给大模型写个带System Prompt的接口让它能连续聊天这就是Agent了。真相差得有点远。聊天机器人是“你问我答”而Agent的本质是一个能自主决策、调用外部工具、根据结果修正自己行为的循环系统。我习惯用一个“反射弧”来类比你伸手拿杯子眼睛看到杯子位置手伸过去碰到烫的杯子会缩手这个“感知—决策—行动—反馈”的闭环就是Agent的基本形态。放到代码里大模型是大脑负责决策工具函数是手脚负责执行返回结果是眼睛负责反馈整个while循环就是神经系统。所以判断你现在做的是不是Agent就看一条你的程序里有没有一个“模型输出—执行动作—把结果喂回去—再让模型决定”的循环。有就是Agent没有顶多算个带业务逻辑的聊天机器人。1.2 一个Agent的五个核心部件理论上讲得再玄落到代码上就五块大模型本体不管你是调GPT、Claude、还是本地部署的Qwen它负责把自然语言变成决策。模型选型直接决定Agent的上限多花点心思在这上面绝对值得。工具集模型不能直接查天气、查数据库、发HTTP请求这些能力全部以函数形式提供给它。工具设计得好坏比提示词写得好坏更影响Agent成败。规划器简单任务模型自己就能一步到位复杂任务需要拆分成多步。有些框架单独做了Plan-and-Execute层但入门阶段直接靠提示词让模型“先列计划再执行”就够了。记忆系统对话上下文是短期记忆向量库里存的业务知识是长期记忆。没有记忆的Agent就像金鱼聊两句就把前面的事忘了。执行反馈与反思工具调失败、参数不合法、结果和预期不符这些反馈必须回到循环里让模型自己判断是重试、换个参数、还是放弃。入门阶段你不用马上把五个部件全做齐但要心里有这张图后面每一块都会用上。1.3 什么时候值得上Agent什么时候别硬上我也劝退过几个想搞Agent的朋友。任务本身如果用几条if-else和正则就能稳定解决那就老老实实写规则别拿大模型去拼。Agent适合的场景有三个特征决策路径不确定、外部信息需要动态获取、步骤之间存在依赖关系。举例说自动巡检一个网站发现异常后要查数据库、查历史告警、再决定发哪种通知——这个路径每次都不一样适合Agent。但如果你只是要把用户输入的手机号校验一下格式写个正则加一个API校验一分钟搞定的事硬套Agent反而引入幻觉和延迟纯属给自己找罪受。2. 动手前的三个关键选择2.1 选模型API派、本地派还是混合派这是你碰到的第一个选择别被网上的争论带着跑根据自己的场景来。API派适合想快速验证业务逻辑的人。OpenAI的gpt-4o-mini、Claude的ha系列、阿里的qwen-plus这些模型推理能力强、工具调用训练充分入门期几乎不用处理“模型不按格式输出”这类问题。缺点是价格在量大的时候肉疼而且数据出境合规问题在B端项目里非常敏感。本地派适合有隐私要求或成本敏感的场景。用Ollama跑Qwen2.5、用vLLM部署Llama好处是数据不出内网、跑多了省钱代价是你要自己搞定显卡资源、推理优化、还有小模型在工具调用上的不稳定——这三大坑任何一个都能让你加班到深夜。混合派是我现在最常用的日常简单任务走本地小模型复杂推理和工具编排走API大模型。中间加一层路由规则按任务难度分流。这个思路你入门阶段可以不做但心里要有个数后面业务体量上来迟早会用到。给你一张对照表按需求直接选维度API派本地派混合派上手速度最快5分钟跑通需要装环境下模型需要先做路由工具调用稳定性高小模型需要调教取决于路由策略单次成本按量付费电费硬件折旧折中数据隐私第三方链路完全内网可配置适合阶段入门、原型验证隐私要求高的业务生产环境2.2 选框架LangChain、LlamaIndex、AutoGen还是裸写框架问题最容易让人纠结我直接给结论入门第一个月别用框架裸写一个循环搞清楚原理第二个月开始用LangChain或你团队熟悉的那套用来省重复劳动但任何时候我都不建议你把业务核心逻辑完全押在一个框架上。框架更新太快了今天这个版本能跑的代码三个月后API整个变脸。LangChain全覆盖但抽象层厚Debug的时候你会觉得在读天书LlamaIndex在RAG场景强Agent编排不是它的主线AutoGen主打多Agent对话入门期容易把人绕晕我建议你至少跑通单Agent再碰多Agent。裸写就是直接调大模型API自己管对话历史和工具调用循环代码量不大但全流程都在你手里出错好排查。我的建议是用裸写入门用LangChain提效最终沉淀自己的一套轻量封装。别把“我会LangChain”当成核心竞争力框架是工具原理才是底子。2.3 环境准备一套能直接跑起来的基线不用一次配齐所有东西先搭最小可用环境。我的建议是Python 3.10以上装好openai客户端和python-dotenv就够起步。如果你要走本地模型路线再装Ollama后续我会单独讲这里先把API这条路跑通。# 建议在虚拟环境里操作 python -m venv agent_demo source agent_demo/bin/activate pip install openai python-dotenv # 如果是本地模型再装 ollama 并用 ollama pull qwen2.5:7b 拉模型代码目录我习惯这样组织简单但清晰后面加工具、加记忆都不会乱agent_demo/ ├── .env # API Key 和 Base URL ├── main.py # 入口 ├── tools/ # 所有工具函数 │ ├── __init__.py │ └── weather.py └── agent/ # Agent 核心循环 ├── __init__.py └── core.py环境里还有几个关键配置要说清楚。.env里不要写死任何密钥这个不用我多强调Base URL要确认好很多国产模型和代理服务都用OpenAI兼容格式填对了就能一套代码到处切模型名也不要写错gpt-4o-mini和gpt-4o-mini-2024-07-18的差价和特性差别很大建议看最新的模型文档再做决定。API密钥的额度管控在生产环境是必须的入门阶段倒不用考虑太深。3. 从零写一个最小Agent手动实现ReAct循环3.1 ReAct本质是“先想一步再做一步”ReAct的论文名字叫“Reason and Act”核心就一句话让模型在每一步交替输出推理和动作而不是憋一个大招一次性给出答案。人遇到复杂问题也是这个流程——先想“这问题缺什么信息”再想“我去哪拿这信息”拿到之后继续往下推。ReAct把这个过程显式化让模型每一步都是可解释的。这个设计有三个直接好处。第一每一步都有迹可循出了问题你回看日志就知道模型在哪一步想岔了第二模型不用一次性把长答案生成完Token消耗更可控生成质量也更高第三你可以在任意一步插入外部反馈比如工具返回结果、异常信息把纯语言模型变成一个能接触真实世界的系统。很多框架的Agent能力都是ReAct的变体你先手写一遍后面看LangChain源码就完全不一样的感觉了。3.2 第一个可用的Agent代码这里我不直接上LangChain就用OpenAI兼容API手写一个最小ReAct循环。这个代码麻雀虽小五脏俱全工具调用、结果回填、步数上限、容错处理都有。我一步步拆开讲你最好跟着敲一遍别光复制。import json from openai import OpenAI # 初始化客户端环境变量里配好 OPENAI_API_KEY 和 OPENAI_BASE_URL client OpenAI() # 工具清单先只放一个查天气的工具 TOOL_SCHEMA { get_weather: { description: 查询指定城市的当前天气情况, params: { city: {type: string, required: True, description: 城市名比如北京、上海} } } } # 工具对应的实际函数 def get_weather(city: str) - str: # 生产环境这里换成真实的天气API比如和风天气、OpenWeatherMap mock_data {city: city, temp: 26, condition: 晴, humidity: 45} return json.dumps(mock_data, ensure_asciiFalse) TOOL_MAPPING { get_weather: get_weather } SYSTEM_PROMPT 你是一个能调用工具的助手。 当需要外部信息时必须严格按以下格式输出 Thought: 你的推理 Action: 要调用的工具名 Action Input: 工具入参的JSON对象 当得到工具结果后继续推理。如果已经能回答用户输出 Final Answer: 你的最终回答 可用工具{tool_descriptions} .format( tool_descriptionsjson.dumps(TOOL_SCHEMA, ensure_asciiFalse, indent2) ) def run_agent(user_query: str, max_steps: int 6) - str: messages [ {role: system, content: SYSTEM_PROMPT}, {role: user, content: user_query} ] for step in range(max_steps): resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, temperature0.2, max_tokens1024, ) reply resp.choices[0].message.content print(f--- Step {step 1} ---) print(reply) # 如果模型给出最终答案直接返回 if Final Answer: in reply: return reply.split(Final Answer:)[-1].strip() # 解析 Thought / Action / Action Input if Action: in reply and Action Input: in reply: try: action reply.split(Action:)[-1].split(\n)[0].strip() input_str reply.split(Action Input:)[-1].strip() action_input json.loads(input_str) except (IndexError, json.JSONDecodeError) as e: # 模型输出格式不对时把错误喂回去让它自己纠正 messages.append({role: assistant, content: reply}) messages.append({role: user, content: f解析工具调用失败{e}\n请严格按照 Thought/Action/Action Input 格式重新输出。}) continue if action in TOOL_MAPPING: try: result TOOL_MAPPING[action](**action_input) except TypeError as e: result f工具参数错误: {e} messages.append({role: assistant, content: reply}) messages.append({role: user, content: f工具 {action} 返回结果{result}}) else: messages.append({role: assistant, content: reply}) messages.append({role: user, content: f工具 {action} 不存在可用工具{list(TOOL_SCHEMA.keys())}}) else: # 模型既没有Final Answer又没有正确格式催它修正 messages.append({role: assistant, content: reply}) messages.append({role: user, content: 请严格按照指定的格式输出不要输出额外内容。}) return 达到最大步数任务终止 if __name__ __main__: result run_agent(北京今天天气怎么样适合穿短袖吗) print( Final ) print(result)跑一下你会发现整个流程完全透明模型先想“需要天气信息”然后调用get_weather工具拿到结果后再判断“26度适合穿短袖”最后给出Final Answer。这个省会需要几步取决于模型能不能一次答全。3.3 为什么先手写而不是直接上框架我知道肯定有人嫌麻烦明明LangChain里一行glm.invoke就能搞定非要手写这个。我为什么坚持让你先裸写因为框架替你隐藏的逻辑才是Agent最核心的逻辑。你把循环自己写一遍才会真正理解几个关键问题系统提示词为什么不能乱写、工具返回结果是怎么进入上下文的、格式错误为什么需要容错、max_steps为什么是必须的。有了这层认知再去读LangChain文档你的心态会完全不一样——你不是在学一个陌生的黑盒而是在看别人怎么把你自己写过的东西抽象包装。而且这个最小循环已经可以打很多业务了。我最早的数据巡检Agent就是这个模式工具换成“查接口”“查配置”“发告警”循环本身一行没改。4. 让Agent真正能干活工具调用与结构化输出4.1 用Function Calling把Agent的“手”接出来手写的ReAct足够理解原理但生产环境我更推荐直接用大模型的Function Calling能力也就是tools参数。它和ReAct的区别在于你不需要让模型自己编格式了API会直接返回结构化的工具调用参数。模型在训练中专门学习过这个任务稳定性比让它手写Action Input高出很多。同样的天气查询用Function Calling的代码清爽得多def run_agent_with_tools(user_query: str) - str: messages [ {role: system, content: 你是助手可以调用工具帮助用户。}, {role: user, content: user_query} ] tools [ { type: function, function: { name: get_weather, description: 查询指定城市的当前天气, parameters: { type: object, properties: { city: {type: string, description: 城市名} }, required: [city] } } } ] for _ in range(5): resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolstools, tool_choiceauto ) msg resp.choices[0].message # 没有工具调用就返回最终回答 if not msg.tool_calls: return msg.content # 模型决定要调用工具把请求追加到对话 messages.append(msg) for tool_call in msg.tool_calls: fn_name tool_call.function.name args json.loads(tool_call.function.arguments) result TOOL_MAPPING[fn_name](**args) # 工具结果用 roletool 追加 messages.append({ role: tool, tool_call_id: tool_call.id, content: result }) return 达到最大重试次数对比一下就知道Function Calling省掉了你自己解析“Action Input”的步骤。模型可能同时要查三个城市的天气它会返回多个tool_calls参数是JSON格式由模型生成你直接loads即可。这在手写版本里要做不少容错在这里模型基本都替你稳住了。4.2 结构化输出与JSON校验工具调用可靠了另一个高频需求是让Agent最终输出结构化的内容。比如我让Agent分析一条日志并给出处理建议如果它输出一大段散文下游系统没法处理。这时要约束它输出JSON。现在主流模型都支持response_format指定json_object或json_schema我建议直接用resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, response_format{type: json_object}, # 有些模型还支持 json_schema可以传更严格的约束 ) data json.loads(resp.choices[0].message.content) # 再补一层字段校验别太信任模型 assert conclusion in data and risk_level in data我踩过的坑是模型偶发会产出非法JSON最常见的就是字符串里的引号没转义、或者多了个逗号。所以解析之后一定要加一层校验和重试逻辑解析失败就把错误信息喂回去让它重新生成。网上很多教程不写这块但生产环境一定要有否则半夜Agent一崩你就得爬起来补数据。4.3 工具返回体设计的约定工具返回结果的格式也会极大影响整体稳定性。我总结了一套自己的约定你直接拿去用也会发现顺手很多返回纯JSON而不是自然语言模型处理结构化数据更稳减少歧义。始终包含status字段成功标success失败标error必要时带error_message。模型看到error信息后可以直接决定是否重试。数据放在data字段里把业务数据和使用模型相关的元信息分开。控制返回体大小单次工具返回超过几千Token的模型会“看不过来”信息被稀释回答质量直线下降。大数据量就截断、分页或者先做摘要再返回。举个例子同样是查询订单列表别让工具返回五十个原始订单而是返回“共120单总金额3800元最近一单是xxx”模型的判断质量会高很多。给模型的信息不是越多越好是越精炼越好。5. 记忆、规划与反思把单步循环升级成能连续干活的Agent5.1 三种记忆的落地方式很多教程把记忆讲得很玄落到实现上无非三种会话记忆最简单就是把历史消息全部传给模型。注意上下文窗口的限制窗口快满时要处理后面第6节会细讲。摘要记忆是当会话太长时让模型把历史浓缩成一段摘要替代原始消息传给后续对话。这个方案有损信息但胜在简单可控。长期记忆就是用向量库存业务知识比如用户偏好、历史工单对话开始时先做一次相似度检索把最相关的几条插进System Prompt或User消息里。我自己的实现方式是三层各司其职Online类短期信息直接塞上下文离线知识存SQLite加向量检索凭证和状态类的强一致信息走Redis。新手不要一上来就搞复杂架构先用LangChain里的ConversationSummaryBufferMemory类似的方案等你的上下文压力真出现了再升级。5.2 规划不是让模型写诗是让模型拆步骤复杂任务让模型一口气完成往往做到第三四步就开始跑偏。这时候需要显式的规划能力。最朴素有效的方法是在任务开始前让模型先输出一个计划清单def plan_task(task: str): plan_prompt f用户任务是{task} 请先拆分任务为最多5个可执行的步骤每一步说明需要调用什么工具。 输出JSON格式{{steps: [{{step: 步骤名, tool: 工具名或不需要, reason: 为什么需要这一步}}]}} # 调用模型解析并存储计划 ...有了计划之后再执行Agent每一步都对照计划走。如果过程中发现某一步结果和预期不符模型会收到反馈并修正剩余步骤。这比没有规划直接硬跑稳定得多你甚至可以先把计划展示给用户确认再由用户确认后执行。很多企业级Agent的做法就是这样的先给你看行动方案等你点头才动。5.3 反思机制一个简单的自检提示词工具全链路跑通之后你开始追求输出的最终质量反思机制是性价比最高的一步。它的实现也异常简单在Agent给出最终答案前多走一次自检reflection_prompt f请检查你刚才给出的答案是否完整、准确。 用户原始问题是{original_query} 你的答案是{agent_answer} 如果答案存在问题请直接输出修正后的最终答案如果没问题保持原答案输出。看上去像是“让模型自我审视”其实效果不错尤其适合总结类、分析类的任务模型在反思轮里经常能补上第一轮遗漏的要点。代价是额外消耗一次模型调用你用的时候要权衡成本和收益。我还见过有人把反思做成多轮评估器让一个模型答题另一个模型打分分数低就重答——这种“判官模式”效果更强但对API稳定性要求也更高入门先不碰。6. 新手最常踩的坑与排查技巧6.1 上下文爆炸你喂进去的比你想的多得多这是新手第一个必然踩的坑。Agent每调用一次工具就要把之前的全部对话历史再加工具结果重新发给模型。如果工具返回本身就几千字五六个来回之后你的上下文就满了。症状是模型开始答非所问或者报“context length exceeded”错误。排查方法很简单把每次请求的messages长度打印出来你就知道上下文是怎么涨上去的。解决方案按优先级排精简工具返回体是最先要做的一次尽量只回真正的关键数据其次是滑动窗口只保留最近N轮对话老消息直接截掉再不行才上摘要记忆把老对话浓缩成摘要再放进去。生产Agent加一层上下文监控是必须的超过阈值自动走压缩或者清空策略别等模型崩了再补救。6.2 明明有工具模型非要硬编答案这是个非常气人的现象工具明明能查模型还是直接编一个“北京今天气温30度”给你。原因通常是三个里至少中一个。一是你的提示词太弱没有明确告诉模型“必须查了工具才能回答”二是工具描述不敏感模型压根没意识到遇到这个问题要调工具你需要把description写得具体比如“当用户询问任何城市的实时天气时必须调用此工具”三是模型本身在Function Calling上训练不足换小参数模型时尤其明显那就只能换模型或者退回ReAct格式。我在实测里发现给工具描述加上“必须”“唯一途径”这类强约束词比在System Prompt里苦口婆心管用得多因为工具描述和工具调用是强相关的高层语义。你还可以设置tool_choice为required强制模型每次必须调用工具代价是每次回复都会触发一次调用成本会略高。6.3 JSON解析失败与并发问题的处理JSON解析失败在上手阶段会频繁遇到解决的套路很固定解析、报错、回喂、重试。我一般写一个可复用的安全JSON解析函数失败了就把异常信息和原文一起丢回模型让它自己修正。注意最多重试两到三次仍失败就走降级流程返回兜底结果别无限循环。并发问题则是把Agent搬到生产环境后的第一道坎。直接同步while循环跑Agent单线程下当然没问题一旦要同时处理几十个请求你要考虑的就不是“Agent怎么聊天”而是“并发怎么扛”——这也是今年社区里讨论非常高频的话题。实践经验是Agent本身不关心并发瓶颈全在下游。模型API限流就让请求排队工具调用有外部依赖就做超时和熔断如果整个Agent循环的思维链状态要跨请求保持需要会话级别的状态存储比如Redis。LLM调用的并发限制最常被忽略你同时开五十个Agent实例第一个报错的就是qps超限。6.4 稳定性三板斧重试、超时、降级总结这两年做Agent的运维经验能稳定生产的Agent不靠快乐调参靠的是工程兜底三件套重试API调用遇到限流或瞬断指数退避重试三到五次。注意Pydantic或requests库里的timeout一定要设默认的无限等待能让你整个Worker卡死。超时工具调用要设定准确超时时间比如HTTP请求10秒、数据库查询15秒。Agent步骤级也要有超时防止模型陷入无意义的长循环。降级模型挂了、工具挂了Agent要有退回方案。我通常的做法是主模型失败切备选模型工具失败返回“当前不可用请稍后”实在不行就转人工工单。别让Agent硬撑着干活它硬撑的结果往往是编一个看起来很真的假结果坑的是后面接管的系统。最后再多说一句我自己测试时定的基线一个生产级别的Agent日志里至少得能看到每轮循环里的“模型输入输出”“工具入参出参”“耗时和Token数”三块信息。有了这套日志上面说的所有坑排查时间都能从小时级降到分钟级。我记得第一次上线数据巡检Agent的时候半夜被值班电话叫醒就是因为没有这个基线抓了一个多小时日志才定位到是工具返回体太大把上下文撑爆了。后来老老实实把日志补上类似的故障基本看一眼就懂了。如果你正打算入坑大模型Agent开发我的建议始终是先别追求多复杂的架构把我们上面这个手写循环跑通、跑稳再慢慢加记忆、加规划、加反思。每一步都亲手实现一遍你踩过的坑会变成你未来排障的直觉。这个领域工具天天换但Agent的底层逻辑不会变把这套基本功打扎实比追着新框架跑要划算得多。