
写这个系列第三篇之前我把后台留言翻了一遍问得最多的不是“怎么调API”而是“Agent一复杂就翻车怎么办”多轮工具调用互相覆盖、上下文一长就失忆、本地能跑的生产环境就崩。这三类问题几乎每个做过Agent的人都会撞上。这篇就专门把硬骨头啃掉——先说Agent主流架构怎么选再把工具调用、记忆系统、多Agent协作的落地细节讲透最后给出从Jupyter脚本到生产服务的完整部署方案。内容偏进阶但我会把每一步的“为什么”也交代清楚方便不同基础的读者都能顺着思路复现。1. Agent主流架构单轮、ReAct、Plan-and-Execute别再只会堆提示词很多人做Agent的第一反应是写一段很长的System Prompt把功能全部塞进去然后让模型直接返回结果。这种做法本质上还是“单轮LLM调用”不是Agent。真正的Agent至少要具备“感知-决策-行动-观察”的闭环能力。先把这个闭环拆清楚才能理解为什么后面几个工程化的设计如此重要。1.1 ReAct循环最基础也最容易被玩坏的架构ReActReasoning Acting是Agent最经典的实现方式模型先推理决定调什么工具拿到工具结果后再推理再调下一个工具直到认为任务完成。这个循环写出来非常短但真正让它稳定的细节都在循环外面。下面是一个功能完整的ReAct简化版我尽量把必要的骨架都保留import json from openai import OpenAI client OpenAI(api_keyYOUR_API_KEY) TOOLS [ { type: function, function: { name: web_search, description: 搜索公开信息返回网页标题、摘要和链接, parameters: { type: object, properties: { query: {type: string, description: 搜索关键词建议使用名词短语} }, required: [query] } } } ] def run_agent(user_prompt: str, max_steps: int 8): messages [{role: user, content: user_prompt}] for step in range(max_steps): resp client.chat.completions.create( modelgpt-4o, messagesmessages, toolsTOOLS, tool_choiceauto, ) msg resp.choices[0].message # 模型不再请求调用工具说明它认为可以给出最终答案了 if not msg.tool_calls: return msg.content # 把模型的“请求动作”追加到对话里 messages.append(msg) # 逐个执行工具调用 for tc in msg.tool_calls: args json.loads(tc.function.arguments) try: result web_search(args[query]) observation {status: ok, data: result} except Exception as exc: # 工具出错时把错误信息当作观察结果还给模型让它自己修正 observation {status: error, message: str(exc)} messages.append({ role: tool, tool_call_id: tc.id, content: json.dumps(observation, ensure_asciiFalse) }) return 达到最大工具调用步数任务未完成这段代码里有几个必须注意的坑第一tool_call_id必须和模型返回的tool_call一一对应拼错了API直接报400。所以工具结果要按tool_calls里的顺序逐条回填不能批量套一个ID。第二工具调用次数必须有上限。我见过很多线上事故都是Agent在一个错误分支里反复调用同一个工具把成本烧爆了。max_steps不是可选项是必需品。第三工具参数用json.loads解析后一定要跑一遍字段校验。模型偶尔会在JSON里塞进多余字段或者把数字写成字符串直接传给底层工具会很危险后面我会专门讲校验方案。第四工具的报错信息要原样返回给模型而不是自己忍住。很多新手喜欢在except里打印日志就完事结果模型不知道工具失败了继续用错误假设往下编产出漏洞百出的结论。把{status: error, message: ...}作为观察结果传回去模型才有机会自我纠正。这是ReAct日志里最能体现“Agent感”的一环。1.2 Plan-and-Execute复杂任务的稳定器ReAct适合“走一步看一步”的动态任务但遇到那种步骤多、容错低的活比如“调研行业现状并生成结构化报告”纯ReAct很容易东一榔头西一棒子。这时候更适合先做计划再执行也就是Plan-and-Execute架构第一轮让模型把任务拆成一个有序的子任务清单然后按清单逐个执行每完成一个子任务就把结果汇总到最终目标里。我实践中常用的简化流程是plan_prompt 请把用户任务拆成不超过5个步骤的子任务清单。 每个子任务必须给出步骤说明、是否需要调用工具、期望输出。 只输出JSON数组不要多余解释。 def plan_and_execute(user_prompt: str): plan_text client.chat.completions.create( modelgpt-4o, messages[{role: user, content: plan_prompt \n\n user_prompt}], response_format{type: json_object}, ) plan json.loads(plan_text.choices[0].message.content) context [] for step in plan[steps]: # 每个子任务都可以套一个ReAct子循环 step_result run_agent( f当前任务{step[description]}\n已知上下文{context}\n子任务期望输出{step[expected_output]} ) context.append({step: step[description], result: step_result}) # 最后汇总所有子任务结果生成最终输出 final_prompt f用户原始需求{user_prompt}\n\n各步骤结果\n{json.dumps(context, ensure_asciiFalse)}\n\n请综合整理成最终交付内容。 return client.chat.completions.create( modelgpt-4o, messages[{role: user, content: final_prompt}] ).choices[0].message.content这套做法的优势非常明显每个子任务的上下文范围被严格限定不会出现某一步跑偏后污染全局中途宕机或模型输出格式失控时可以断点续跑子任务之间天然可以拆给不同模型、不同并发度去执行。代价是总Token消耗明显上升且计划一旦制定后不太容易根据中途信息动态调整。如果任务对实时性要求高、前面几步的结果可能否定整个计划Plan-and-Execute就会很僵化。1.3 架构选型决策表没有银弹按场景配比我在实际项目里很少只用一种架构通常是混合的先Plan定骨架子步骤用ReAct去执行执行到一半发现新信息就触发“重新计划”。下面这个表是我选择架构时的基本判断逻辑任务特征推荐架构理由多步工具调用目标单一明确ReAct灵活、成本低能根据每一步结果动态调整长流程、多阶段、输出结构化文档Plan-and-Execute步骤可控中途可复盘可断点续跑客服对话、临时任务穿插ReAct 记忆检索需要动态切换话题又要依赖历史信息数据收集后分析再生成报告Plan ReAct子循环计划保证覆盖度子任务内用ReAct消化不确定性任务本身很简单只调一两次工具单轮Function Call直接调工具不需要Agent循环省Token还省延迟多说一句很多框架把ReAct吹得神乎其神但真实场景里40%的需求用“单轮Function Call 一个完善的后处理函数”就能解决根本不需要Agent循环。不要为了“Agent”而Agent架构越简单线上越好维护。这也是我前面强调“别只会堆提示词”的原因——把任务拆成“需要用Agent的部分”和“不需要用Agent的部分”比让模型把所有事都干完更稳定。2. 工具调用工程化写清SOP、管好异常、守住安全边界Agent的能力上限很大程度取决于你能给它多少靠谱的“手”。而手的好坏不在函数名字好不好听而在工具描述、参数定义和错误处理这三层工程细节。2.1 工具描述要像给实习生写SOP而不是给老同事写备注模型不熟悉你的业务它只是一个“精于字面匹配的决策者”。工具描述写得越模糊模型越容易用错参数。我见过一个典型的反例某团队封装了一个发邮件的工具描述写的是“发送邮件”参数是to、content结果模型经常把收件人姓名传进to因为描述里没说明格式。正确做法是把单位、格式、边界、甚至一个示例都写进去{ type: function, function: { name: send_email, description: 发送一封文本格式邮件。收件人必须是完整的邮箱地址多个收件人用逗号分隔。禁止用收件人姓名代替邮箱地址。, parameters: { type: object, properties: { to: { type: string, description: 收件人邮箱示例aliceexample.com,bobexample.com }, subject: {type: string, description: 邮件主题不超过100个字}, content: {type: string, description: 邮件正文纯文本} }, required: [to, subject, content] } } }这样的描述本质上是一份SOP明确“怎么做”、明确“不能怎么做”、给了一个示范。不要小看这几行字它往往能把工具调用成功率从70%拉到95%以上。2.2 用Pydantic统一管理工具定义与入参校验手写JSON Schema又一个很大的问题模型传参和实际函数签名一旦对不上只能在运行时崩溃。我的做法是用一个工具注册器函数参数直接定义成Pydantic模型然后自动生成JSON Schema。这样函数签名、校验规则、Schema描述三处不会出现不一致。from pydantic import BaseModel, EmailStr, Field from typing import Any, Callable class SendEmailInput(BaseModel): to: str Field(description收件人邮箱多个用逗号分隔) subject: str Field(description邮件主题) content: str Field(description邮件正文) class ToolRegistry: def __init__(self): self._tools {} self._schemas [] def register(self, schema_cls: type[BaseModel]): def decorator(func: Callable): name func.__name__ result_cls self._to_json_schema(schema_cls) result_cls[name] name result_cls[description] func.__doc__ or self._tools[name] (schema_cls, func) self._schemas.append({type: function, function: result_cls}) return func return decorator def call(self, name: str, args: dict): schema_cls, func self._tools[name] parsed schema_cls(**args) # 校验失败会抛异常 return func(**parsed.model_dump()) def _to_json_schema(self, schema_cls): schema schema_cls.model_json_schema() parameters {k: v for k, v in schema.items() if k ! title} return {parameters: parameters}这套结构有三个隐形的收益一是新增工具只需要写一个函数、一个Schema类不容易漏二是模型调用时即使传了多余字段Pydantic默认会忽略不会污染底层逻辑三是校验失败抛出的错误能被上一章的ReAct循环捕获并反馈给模型形成自我修正闭环。2.3 工具错误处理与安全边界让模型试错但别让它乱来工具调用错误处理的核心心法只有一条把错误当作观察结果而不是程序异常。模型是一个“推理器”你需要给它完整的感知信息它才能做出正确决策。工具失败了、参数非法、外部服务超时都要想办法转成结构化消息回填给模型。但安全边界绝对不能靠模型自觉。LLM不是可信执行环境绝不能因为“模型判断没问题”就直接放行写操作。我自己的项目里定了这么几条红线所有写操作发邮件、改数据库、删文件必须登记白名单动态参数一律走模板校验。凡是会真实影响外部系统的动作Agent只能产出“拟执行内容”由人工审核后触发。工具结果里涉及隐私、密钥的字段在回传给模型之前要做好脱敏。给每个工具设置独立的执行超时避免一个慢接口拖垮整个Agent循环。举个制造过事故的例子某Agent在回复用户时被诱导调用“删除项目”工具因为工具描述正好只写了“根据参数删除项目”模型也没意识到底层影响。现在我会在描述里加上“本操作不可逆调用前必须确认用户明确输入了项目名且只涉及测试环境”同时在工具函数内部强制二次确认参数包含特定标识。这种“描述约束代码防御”的双保险比单纯指望模型守规矩可靠得多。3. 记忆系统短期上下文、长期向量库、结构化事实三件套Agent一长就“失忆”几乎是所有人的共同痛点。它的根源很简单LLM的上下文窗口有限而真实业务里的历史信息是无限增长的。所以记忆系统要解决的问题不是“能不能记”而是“该记什么、忘什么、从哪找回”。3.1 三种记忆的分工我把Agent的记忆拆成三类和人类记忆做类比类型载体作用典型容量工作记忆当前对话的messages数组维持当前任务的上下文连贯性视模型窗口而定一般2K-200K tokens长期记忆向量数据库 摘要跨会话保留事实、偏好、历史结论可无限扩展程序记忆代码、配置、规则固化工具、流程、权限边界版本化管理很多人把“记忆”和“上下文”混为一谈这是认知上的误区。上下文只是当前任务的一次性草稿纸草稿纸写满就该归档长期记忆才是Agent持久价值的来源。比如一个售后支持Agent它真正需要长期记住的不是每一条客流记录而是“这个客户是VIP偏好邮件联系上次沟通中明确拒绝过电话推销”这类结构化事实。3.2 上下文窗口管理的三种策略工作记忆不可能无限制增长我的日常处理方案有三种按任务复杂度组合使用策略一滑动窗口。只保留系统提示、最近N轮对话、以及当前正在处理的工具结果。最省Token但会丢早期关键信息。策略二摘要压缩。当消息总数超过阈值时把旧消息丢给模型生成一段摘要替换掉原始消息。损失细节但保住主线。策略三检索增强。每次收到用户新消息之前先从长期记忆里检索和当前意图最相关的片段拼接到上下文里。这是最“Agent”的做法适合跨会话场景。一个比较通用的伪代码如下def build_context(user_message, session_history, memory_store, max_context_tokens8000): # 1. 取最近若干条历史 recent session_history[-4:] # 2. 从长期记忆里检索与本次意图相关的片段 related_memories memory_store.search(user_message, top_k3) # 3. 拼装系统提示 相关记忆 最近历史 当前消息 return { system: SYSTEM_PROMPT, memory: related_memories, history: recent, current: user_message }这里有个重要细节检索必须在“构造上下文”之前完成因为你要用“当前用户消息”去检索记忆。而历史消息本身不需要全部参与检索否则既费Token又容易带回噪声。我的经验是检索时把“结构化事实”单独抽出来存储和“对话痕迹”分开——对话痕迹适合用向量检索结构化事实更适合用SQL精确查询。3.3 长期记忆落地向量库和结构化存储各司其职长期记忆我通常做成两层第一层是向量库存对话摘要和文本信息第二层是关系型数据库存可量化的用户事实、项目状态、任务偏好。原因很简单向量检索擅长“模糊匹配”但“这个客户上次买的套餐是什么”这种精确查询用向量去做又慢又不准。以Chroma为例一个轻量的长期记忆实现import chromadb from chromadb.utils import embedding_functions client chromadb.PersistentClient(path./memory_store) col client.get_or_create_collection( nameconversation_memories, embedding_functionembedding_functions.DefaultEmbeddingFunction() ) def save_memory(session_id: str, content: str, meta: dict): col.add( ids[f{session_id}-{hash(content)}], documents[content], metadatas[{session_id: session_id, **meta}] ) def search_memory(query: str, top_k: int 3): results col.query(query_texts[query], n_resultstop_k) return [doc for doc in results[documents][0]]写入记忆的时机很关键。我的习惯是每个Agent循环结束后把“结论性内容”而不是“过程性内容”写入长期记忆。比如写报告Agent过程里搜了20个网页只需要抽取核心结论存向量中间那些“某链接打开失败”“搜索关键词重试两次”之类写进去只会污染检索结果。另外一个容易被忽视的坑长期记忆写入时要带时间戳和来源。否则当任务涉及“最近一周的数据”时Agent从记忆库里翻出三个月前的旧结论当新事实用后果很严重。所以每次save_memory我都会在meta里加created_atISO格式字符串检索时也会在提示词里明确“优先采用最新时间戳的记忆”。4. 多Agent协作监督者、流水线、市场式三种模式怎么落地单Agent的能力有上限但盲目堆多Agent只会让系统更脆弱。多Agent协作的本质是“合理的任务拆解 清晰的消息协议 可追踪的执行链路”。把这三件事做好比Agent数量重要得多。4.1 三种主流协作模式我归纳了三种可落地的协作模式分别对应不同场景监督者模式Orchestrator-Worker一个中枢Agent负责任务规划、派发和结果汇总多个Worker分别执行子任务。适合大多数生产场景。优点是职责清晰、好管控缺点是中枢Agent可能成为性能瓶颈。流水线模式Pipeline任务按照固定顺序依次经过多个Agent每个Agent只处理特定环节。适合流程固定的任务比如“清洗数据 → 特征分析 → 生成图表 → 撰写结论”。优点是单点逻辑简单缺点是链路中某环节失败会阻断整个流程。市场/黑板模式Blackboard多个Agent共享一个任务面板谁有能力认领谁做。适合开放式探索场景比如代码评审中让多个“专家视角”Agent各自发言。控制难度最高实际项目慎用。我自己的原则能不用多Agent就不用。只有当一个任务确实由多个职责差异巨大的子任务组成并且这些子任务能并行或必须隔离上下文时才考虑拆Agent。拆出来的好处是“上下文隔离专注度提升”代价是“消息复制多轮Token膨胀”。4.2 消息协议与任务编排多Agent之间通信切忌直接传“字符串”。否则你根本查不清一个结论是哪个Agent、哪一轮、基于哪些上下文生产的。我常用的消息结构是一个Dataclassfrom dataclasses import dataclass, field from datetime import datetime from typing import Any dataclass class AgentMessage: msg_id: str task_id: str sender: str receiver: str payload: dict created_at: str field(default_factorylambda: datetime.utcnow().isoformat())每个字段都有它的用途msg_id用于全局追踪task_id串起一轮完整任务sender/receiver让链路清晰payload承载结构化数据。这样在日志系统里只要按task_id过滤就能看到整个Agent协作的完整链路——排查问题的时候这条链路等于救命稻草。然后是一个简化版监督者模式的编排骨架def orchestrator(user_request: str): workers {researcher: call_researcher, analyst: call_analyst, writer: call_writer} plan generate_plan(user_request) # 返回子任务列表 results {} for subtask in plan: worker_name subtask[worker] msg AgentMessage( msg_idfmsg_{subtask[step_id]}, task_idftask_{uuid4().hex[:8]}, senderorchestrator, receiverworker_name, payload{request: subtask[description], deps: results} ) results[worker_name] workers[worker_name](msg) return assemble_output(results)编排中最容易踩的坑是死循环。比如一个Worker发现自己缺资料向另一个Worker请求帮助另一个Worker又反过来请求两个Agent来回发消息出不来。我通常会给整个编排流程设一个全局步骤上限超过上限直接终止并返回“任务过于复杂需要人工介入”。这个“人工介入”不是丢人多Agent系统本身就该有fallback到人的路径。4.3 多Agent系统的成本与稳定性如果说单Agent耗费的Token已经让你心疼那么多Agent能让你心疼到麻木。每多一个Agent意味着多一轮模型调用、多一次上下文组装、多一份重复的系统提示词。这些成本都是线性的但问题排查难度是指数上升的。成本上我最常用三种手段模型分级简单Worker用便宜小模型只有规划、汇总这种复杂任务才上最强模型。结果缓存Worker输出如果无状态、可哈希就按输入内容哈希做结果缓存同子任务不重复执行。延迟调用Orchestrator不是每次都把全部子任务发出去而是“按需拉取”等前置结果真的被用到时再触发下一步。稳定性上最重要的一点是每个Worker的输出都要做“格式校验”而不是直接信它。我的Worker函数结尾都有统一的validate_output检查返回JSON是否符合预定义的输出Schema不符合就重试一次重试再失败就标记为失败让Orchestrator决定是跳过还是找人类。5. 部署与服务化状态持久化、并发控制和可观测性一个都不能少从“我笔记本上能跑”到“线上能稳定扛流量”中间差的不是代码量而是几个工程意识。这一章我把最关键的三个问题讲透。5.1 项目结构别再让一切逻辑都堆在Jupyter里一个可以部署的Agent项目目录结构至少要让人一眼看出“入口、工具、Agent、存储”四层agent_project/ ├── main.py # FastAPI入口 / CLI入口 ├── agent/ │ ├── orchestrator.py # 编排逻辑 │ ├── memory.py # 长期记忆读写 │ └── loop.py # ReAct循环 ├── tools/ │ ├── registry.py # 工具注册器 │ ├── search.py │ └── database.py ├── schemas/ │ └── models.py # Pydantic数据结构 ├── storage/ │ ├── vector_store.py │ └── session_store.py └── config.py # 配置项集中管理这个结构的意义在于工具、Agent逻辑、存储三者解耦。替换一个向量库或者换一个模型底座不需要动Agent循环代码。很多项目做到中期重构就是因为前期把所有函数都写在一个脚本里改一行配置都要全局搜。API层我喜欢用FastAPI因为异步支持和Pydantic集成天然适合Agent服务from fastapi import FastAPI from pydantic import BaseModel app FastAPI() class ChatRequest(BaseModel): session_id: str message: str stream: bool False app.post(/agent/chat) def chat(req: ChatRequest): # 从存储加载session状态构建上下文执行Agent循环写回状态 return run_agent_session(req.session_id, req.message, req.stream)如果你是Django项目也可以直接把Agent逻辑封装成一个service层放在视图函数里调用本质一样只是把API层从FastAPI换成Django view。5.2 状态持久化与并发控制最典型的部署事故是把session状态放在Python进程的全局字典里。一旦开了多Worker进程或负载均衡用户请求打到不同进程上下文直接错乱。正确做法是把会话状态放到外部存储。我这里有一套现成的持久化方案短期工作记忆半个小时内会频繁读写放Redis用session_id做key存最近的messages列表TTL设为30分钟。长期记忆放向量库加关系型数据库跨session保留。任务锁对同一个session_id的并发请求加分布式锁避免多个请求同时读写同一个Agent会话。并发控制常被忽略。用户连按两次发送两个请求同时操作同一个session会出现消息顺序错乱、工具结果张冠李戴。用redis.setnx做锁是最省事的import redis r redis.Redis(hostlocalhost, port6379) lock_key fagent_lock:{session_id} acquired r.set(lock_key, 1, nxTrue, ex60) if not acquired: return {error: 上一轮任务还在处理中请稍候}这个逻辑虽然简单但能挡住绝大多数并发引起的脏读脏写。5.3 可观测性与成本控制部署之后你迟早会遇到同一个问题“Agent上一轮干了什么为什么结论这么奇怪”没有可观测性你只能对着屏幕发呆。我最低要求的可观测性有四条缺一不可结构化日志每条Agent日志至少包含task_id、session_id、step、tool_name、tokens_used、latency_ms。调用追踪把Agent内部的每次LLM调用、工具调用都打点能按task_id串成树状链路。结果评测集准备一组固定的测试用例每次改完Agent逻辑都要跑一遍防止“改一处坏全局”。成本报表按session_id或task_id汇总Token消耗每天出报表。不看成本你永远不知道多Agent到底烧了多少钱。Token计数这块顺带一提LLM按token计费的原理是把文本切成子词中文一个字通常对应1到2个token一段千字中文大概在1500-2000 token左右。所以一个多Agent任务跑下来几十万token是稀松平常的事。这也是为什么我前面建议模型分级、结果缓存——成本控制不是抠门是让Agent能活到明天。评测集很容易被忽略但它比日志还重要。我建议固定场景不少于20条覆盖“单独调用工具”“多轮工具联动”“错误恢复”“上下文重写”四类典型情况。每次改Prompt、改工具描述都跑一遍这20条看工具调用准确率和最终任务完成率的变化。这比人工随机测试靠谱得多。6. 实战串联一个自动研究报告Agent的完整骨架最后一个章节我把前面讲的架构、工具、记忆、多Agent协作、部署一次性串起来做一个自动研究报告生成Agent。这个Agent的需求很典型用户丢过来一个话题它要自动搜集公开资料、分析要点、生成一篇带结论的报告。6.1 系统组成与职责划分整个系统分成四层Orchestrator监督者接收用户话题把它拆成三个子任务依次派发给三个Worker。Researcher Worker负责搜集公开网页资料并把每次搜索得到的结论写入长期记忆。Analyst Worker读取长期记忆中的搜索结果提炼关键观点、时间线、争议点。Writer Worker基于Analyst输出生成结构化研究报告报告格式定义为Pydantic模型。数据流非常简单user_request → orchestrator → plan → worker_chain → final_report。但每个Worker内部都可以有自己的ReAct子循环比如Researcher会连续搜索多个关键词。6.2 核心数据结构from pydantic import BaseModel, Field from typing import List class ResearchPlan(BaseModel): topic: str Field(description研究报告主题) subtopics: List[str] Field(description需要调研的子话题列表) class ReportSection(BaseModel): title: str content: str key_facts: List[str] class FinalReport(BaseModel): topic: str summary: str sections: List[ReportSection] disclaimer: str 本报告由AI Agent自动生成重要决策请人工复核。强制让Writer输出结构化的FinalReport有两个好处一是后续入库、渲染、比对都方便二是结构约束能让模型“回答得更像报告而不是流水账”。6.3 把记忆和工具串进来系统运行到这个阶段长期记忆存储的完整链路是这样的def run_researcher(topic: str): search_query f{topic} 最新进展 2025 for _ in range(3): result call_web_search(search_query) if result.status ok: save_memory( session_idtopic, contentresult.snippet, meta{source: result.url, created_at: now()} ) return summarize_search_result(result) return {error: 连续三次搜索失败}这里的save_memory直接把检索回来的公开信息片段存入向量库。Analyst启动时会用search_memory(topic)拉取关联片段跨过“必须把所有原始数据塞进上下文”的笨办法。6.4 部署形态和扩展方向这个Agent部署成HTTP服务后用户只需要提交一个topic轮询等待报告生成。生产环境可能会加一个任务队列用一个后台worker消费消息因为研究报告生成耗时较长不适合HTTP请求同步等待。往后的扩展方向也很明确一是给Researcher增加更多信息源工具二是在Analyst和Writer之间加一层人工审核让Agent先产出草稿再由人确认三是把FinalReport结构从Markdown升级成带最新时间戳的持久化数据支持按主题回查历史报告。每一步扩展都是在前面的骨架上做加法不需要推翻重写。这篇写到这里我自己最深的体会是Agent开发真正难的不是让它“跑起来”而是让它在多变、易错、有成本压力的现实环境里“可靠地跑”。把架构选型想清楚、工具边界守好、记忆分好层、协作协议定明白、部署可观测剩下的就是反复用评测集打磨Prompt和工具描述。这套方法论是我踩了无数坑之后沉淀下来的照着做至少能少走一半弯路。