
1. 从“看课”到“上手”Agent 开发到底在做什么真正开始写 Agent 之前我对它的理解停留在“会调用工具的聊天机器人”这个层面。知乎上那门 AI 应用开发课我刷了两遍笔记记了一大本Function Calling 的格式、ReAct 的循环、记忆管理的分层考试我都能默写。但第一次自己动手搭一个能跑通完整任务的 Agent还是卡了整整三天。问题不在于某个 API 不会调而在于课程里那些看起来“像理论”的东西——工具描述怎么写、记忆什么时候存什么时候取、多 Agent 之间怎么传话——全都是实际开发中绕不开的硬骨头。你少写一句工具描述模型就选错函数你记忆策略设计得粗糙Agent 跑三轮就开始胡言乱语你不做多 Agent 的职责切分一个 Agent 又当爹又当妈最后什么都做不好。这篇内容就是把我从“课程学习者”到“能交付一个可用 Agent”之间踩过的坑、验证过的方案、以及那些课程里一笔带过但实际开发中至关重要的细节完整地拆开讲一遍。适合已经了解 Agent 基本概念、准备动手做第一个真实项目的开发者也适合做过一两个 Demo 但总觉得“跑不稳”的人。我会围绕 Function Calling、MCP、记忆管理、多 Agent 协作这几个核心点结合我实际项目中的代码和配置把每个环节的“为什么”和“怎么做”都说清楚。2. Function Calling不是会调就行工具描述才是命门2.1 工具描述为什么比函数实现更重要很多人第一次写 Function Calling注意力全在函数体上——参数怎么解析、返回值怎么拼。但实际跑起来你会发现模型选不选这个工具、选得对不对八成取决于你写在description里的那几行字。我做过一个对比实验同一个查询天气的函数两种描述方式。第一种课程里常见的写法{ name: get_weather, description: 获取天气信息, parameters: { type: object, properties: { city: { type: string, description: 城市名 } } } }第二种我实际项目中改过的写法{ name: get_weather, description: 查询指定城市当前及未来三天的天气状况包括温度、湿度、风力、降水概率。当用户询问天气、气温、是否下雨、穿衣建议时使用此工具。不适用于查询历史天气或气候统计数据。, parameters: { type: object, properties: { city: { type: string, description: 城市名称需为中文全称如杭州市而非杭州或Hangzhou }, date_range: { type: string, enum: [today, next_3_days], description: 查询的时间范围默认为today } }, required: [city] } }同样的模型、同样的用户问题“明天出门要不要带伞”第一种描述下模型有大约三成概率去调用一个无关的搜索工具第二种描述下连续测试五十次全部正确命中。差别就在描述里我明确了“什么时候用”“什么时候不用”“参数格式要求”。工具描述的本质是给模型写一份“使用说明书”不是给它看你的函数签名。模型没有你的代码上下文它只能靠描述来判断这个工具能不能解决当前问题。2.2 参数设计的三个实操原则参数设计上我总结了三条原则都是踩坑踩出来的。第一能用枚举就不用自由文本。上面例子里的date_range我用了enum模型输出的时候就不会给你编出“未来五天”这种你根本没实现的选项。自由文本参数看起来灵活实际上是在给模型挖坑它填什么你都得兜着。第二必填参数越少越好。我见过一个工具定义了七个必填参数结果模型每次调用都要“思考”很久还经常漏填。后来我把其中四个改成可选、给了默认值调用成功率直接从六成出头拉到九成以上。模型在生成参数时是有“注意力预算”的必填项太多它会顾此失彼。第三参数描述里写清楚格式和边界。比如日期参数你要写“格式为YYYY-MM-DD”否则模型可能给你“2024年3月5日”或者“March 5th”。数值参数要写“取值范围1-100”不然它可能传个0或者负数进来。2.3 多工具场景下的选择逻辑当你有十几个工具的时候Function Calling 的准确率会明显下降。我实测过一个包含十五个工具的场景模型选错工具的概率接近两成。后来我用了两个策略来改善。一是工具分组。把功能相近的工具归到一个“工具集”里先让模型选工具集再在工具集内部选具体工具。相当于把一次十五选一变成两次“五选一”和“三选一”准确率提升很明显。二是动态工具注入。不是每次请求都把全部工具塞给模型而是根据对话上下文先做一轮粗筛。比如用户明显在聊日程管理那就只注入日历相关的五六个工具。这个粗筛可以用简单的关键词匹配也可以用一个小模型来做成本很低但效果很好。# 动态工具注入的简化实现思路 def select_tools(user_input, all_tools, tool_groups): # 先用关键词做一轮粗筛 matched_groups [] for group_name, keywords in tool_groups.items(): if any(kw in user_input for kw in keywords): matched_groups.append(group_name) # 如果没有匹配到返回默认工具集 if not matched_groups: return all_tools[:5] # 只返回最常用的几个 # 返回匹配组内的工具 selected [] for tool in all_tools: if tool[group] in matched_groups: selected.append(tool) return selected这个思路不复杂但实际效果立竿见影。工具数量从十五个降到五六个之后模型的选择准确率回到了九成以上。3. MCP让 Agent 真正“接上”外部世界的那根线3.1 MCP 到底解决了什么问题MCP 刚出来的时候我看了一圈介绍感觉像是“又一个协议”没太当回事。直到我要把一个 Agent 接到本地的数据库和几个内部系统上才发现没有 MCP 的话每个工具你都得自己写适配层——数据库一个连接方式、文件系统一个、浏览器一个代码里全是胶水逻辑。MCP 的核心价值在于标准化。它定义了一套 Agent 和外部工具之间的通信规范工具提供方只需要实现一个 MCP Server任何支持 MCP 的 Agent 都能直接调用。你不用再为每个工具写一遍“怎么连、怎么调、怎么解析返回值”。我实际用下来MCP 最舒服的场景是浏览器操作和本地文件系统。比如 Playwright MCP装好之后 Agent 就能直接控制浏览器打开页面、点击元素、截图、提取内容你不需要在 Agent 代码里写任何 Playwright 的调用逻辑。文件系统 MCP 也一样Agent 可以直接读写本地文件你只需要在配置里声明允许访问的目录。3.2 MCP Server 的配置与接入实操以 Playwright MCP 为例说下我实际接入的步骤。首先是在 Agent 框架的配置文件中声明 MCP Server。不同框架的配置格式略有差异但核心信息就三样Server 的启动命令、参数、以及需要注入的环境变量。{ mcpServers: { playwright: { command: npx, args: [-y, anthropic/mcp-playwright], env: { BROWSER: chromium, HEADLESS: true } } } }配置好之后Agent 启动时会自动拉起这个 MCP Server然后通过标准输入输出和它通信。你不需要手动去启动 Server框架会帮你管理生命周期。接入之后Agent 就多了几个工具browser_navigate、browser_click、browser_screenshot、browser_extract_text等等。这些工具的描述和参数都是 MCP Server 提供的你不需要自己写 Function Calling 的定义。一个容易踩的坑MCP Server 启动失败的时候很多框架不会给你明确的报错只是工具列表里少了几个工具。我建议在 Agent 初始化之后加一段代码打印一下当前可用的工具列表确认 MCP Server 真的连上了。3.3 MCP 工具和原生 Function Calling 怎么选实际项目中我经常混用两者。判断标准很简单这个工具是不是通用的、别人也可能用到的如果是优先找现成的 MCP Server如果是业务特有的逻辑那就自己写 Function Calling。比如“查询订单状态”这种业务接口肯定自己写 Function Calling。但“读取 PDF 文件内容”这种通用能力直接用 MCP 的 filesystem Server 就行没必要自己造轮子。另外 MCP 还有一个好处是工具描述的质量有保障。自己写 Function Calling 的时候描述写得好不好全看个人水平。MCP Server 通常是社区维护的工具描述经过多人使用和迭代质量普遍比自己随手写的要高。4. 记忆管理Agent 从“金鱼脑”到“有记性”的关键4.1 短期记忆和长期记忆的分层设计Agent 的记忆管理是我踩坑最多的部分。最开始我图省事把全部对话历史都塞进上下文结果跑个十几轮之后 token 就爆了而且模型开始“忘记”前面说过的关键信息——因为上下文太长注意力被稀释了。后来我参考课程里提到的分层思路把记忆拆成三层。第一层是工作记忆就是当前这一轮对话的上下文。这部分必须完整保留因为模型需要它来理解当前在做什么。但我会做一个滑动窗口只保留最近 N 轮N 一般设五到八轮。第二层是会话记忆把整个会话中的关键信息抽取出来存成一个结构化的摘要。比如用户说“我明天要去杭州出差”工作记忆里可能几轮之后就滑出去了但会话记忆里会存一条“用户计划明天前往杭州”。这个摘要每几轮更新一次用一个小模型来做抽取。第三层是长期记忆跨会话持久化的信息。比如用户的偏好、常用地址、历史任务记录。这部分存在外部存储里需要的时候通过检索召回。# 记忆分层管理的简化结构 class AgentMemory: def __init__(self): self.working_memory [] # 最近N轮对话 self.session_summary {} # 当前会话的关键信息 self.long_term_store {} # 持久化存储 def add_message(self, role, content): self.working_memory.append({role: role, content: content}) # 保持工作记忆不超过窗口大小 if len(self.working_memory) self.window_size * 2: self.working_memory self.working_memory[-self.window_size * 2:] def update_session_summary(self, new_info): # 用模型抽取关键信息并合并到摘要中 self.session_summary.update(new_info) def recall_long_term(self, query): # 根据当前上下文检索长期记忆 relevant self._search_long_term(query) return relevant4.2 记忆写入和召回的时机判断分层结构搭好之后下一个问题是什么时候写、什么时候读。写入时机我总结了两条规则。一是信息密度触发当用户输入或 Agent 输出中包含实体人名、地点、时间、数字或者明确的偏好表达时触发一次记忆抽取。二是轮次触发每五轮对话做一次全量摘要更新防止遗漏。召回时机更微妙。我的做法是在每次生成回复之前用当前用户输入去检索长期记忆把相关度高的几条注入到系统提示里。但要注意控制数量一般不超过三条否则会干扰模型对当前上下文的判断。一个实测有效的技巧在系统提示里明确告诉模型“以下是从长期记忆中检索到的相关信息可能对当前对话有帮助但请以当前对话内容为准”。这句话能显著降低模型被旧记忆带偏的概率。4.3 记忆冲突和过期的处理记忆管理里最头疼的是冲突。用户上周说“我住在北京”这周说“我搬到上海了”两条记忆都在库里Agent 到底信哪个我的处理方式是给每条记忆加时间戳和置信度。时间戳用于判断新旧置信度用于判断可靠性。当检测到冲突时优先采用时间更新的记忆同时把旧记忆标记为“可能过期”而不是直接删除。如果后续对话中用户再次确认了旧信息可以恢复其置信度。# 记忆冲突处理的简化逻辑 def resolve_conflict(new_memory, existing_memories): for existing in existing_memories: if is_conflicting(new_memory, existing): if new_memory[timestamp] existing[timestamp]: existing[status] possibly_expired return new_memory else: new_memory[status] pending_confirmation return existing return new_memory这套机制跑下来Agent 在长对话中的表现稳定了很多。之前跑十轮就开始胡言乱语的情况现在跑到三四十轮还能保持上下文一致。5. 多 Agent 协作从“一个人干所有事”到“各司其职”5.1 什么时候需要多 Agent不是所有任务都需要多 Agent。我一开始也觉得多 Agent 很酷什么都想拆成多个角色结果发现大部分场景下单 Agent 加多工具就够了。多 Agent 真正有价值的场景是任务可以明确分成不同专业领域且各领域之间需要反复交互。比如我做过一个“技术文档写作”的 Agent 系统拆成了三个角色研究员负责搜集资料、写手负责组织内容、审校负责检查事实和表达。这三个角色的工具集和提示词差异很大研究员需要搜索和阅读工具写手需要文本生成和格式化工具审校需要事实核查和对比工具。如果用一个 Agent 来做提示词会变得极其臃肿模型经常“串戏”——该核查的时候在生成该生成的时候在搜索。5.2 角色切分和通信机制的设计角色切分的原则是每个角色的职责可以用一句话说清楚。如果一句话说不清说明切分得不够干净。上面例子里“研究员负责找到可靠的相关资料”一句话就说清了不需要再解释。通信机制上我用的是共享黑板模式。每个 Agent 把自己的输出写到一块共享的工作区里其他 Agent 可以读取。这样不需要 Agent 之间直接对话降低了耦合。# 共享黑板模式的简化实现 class SharedWorkspace: def __init__(self): self.artifacts {} # 各Agent产出的内容 def write(self, agent_name, key, content): self.artifacts[f{agent_name}:{key}] { content: content, timestamp: time.time(), agent: agent_name } def read(self, key): return self.artifacts.get(key) def get_all_by_agent(self, agent_name): return {k: v for k, v in self.artifacts.items() if v[agent] agent_name}编排逻辑上我用了一个简单的状态机来控制流程研究员完成资料搜集后触发写手开始工作写手完成初稿后触发审校审校发现问题则回退到写手没问题则输出最终结果。状态机用代码写死不交给模型来判断这样流程更可控。5.3 多 Agent 协作中的常见故障多 Agent 系统最容易出的问题是死循环。审校说“第三段数据不对”写手改了之后审校又说“改完之后逻辑不连贯”写手再改审校再挑毛病来回五六轮都结束不了。我的解决办法是加两个限制。一是最大轮次限制审校和写手之间的交互最多三轮超过就强制输出当前版本并标记“待人工复核”。二是问题升级机制如果审校连续两次提出同类问题说明写手可能理解不了这个反馈这时候把问题升级给一个“协调者”角色由协调者来决定是降低标准还是换一种方式修改。另一个常见问题是信息在传递中失真。研究员找到的资料经过写手改写、审校修正之后可能已经偏离了原始资料的意思。我的做法是在共享黑板里保留原始资料的引用链接审校在核查时必须对照原始资料而不是只看写手的版本。6. 常见问题与排查技巧实录6.1 Function Calling 相关故障速查现象可能原因排查方法解决思路模型不调用工具直接回答工具描述不够明确检查描述里是否写了“什么时候用”补充使用场景和触发条件调用了错误的工具工具之间描述太相似对比几个工具的 description差异化描述明确各自边界参数格式错误参数描述缺少格式说明看模型传了什么格式的参数在描述里写明格式和示例工具调用后模型不继续返回值格式模型看不懂检查返回的 JSON 结构简化返回值只保留关键字段多轮对话后工具选择变差上下文太长稀释了注意力看当前上下文 token 数做上下文压缩或工具动态注入6.2 MCP 接入的坑与解法MCP Server 连不上是最常见的问题。我的排查顺序是先确认 Server 命令能不能在终端里手动跑起来再确认 Agent 框架的配置文件路径和格式对不对最后看框架日志里有没有 MCP 相关的报错。有一个很隐蔽的坑是环境变量传递。有些 MCP Server 依赖特定的环境变量才能正常工作但框架在启动 Server 时可能没有继承你当前 shell 的环境变量。解决办法是在配置文件的env字段里显式声明所有需要的环境变量。还有一个坑是MCP Server 的版本兼容性。不同版本的 Server 可能对框架版本有要求我遇到过升级框架之后旧版 MCP Server 就连不上的情况。建议在配置文件里锁定 MCP Server 的版本号不要用latest。6.3 记忆管理的典型问题记忆召回不准确是最常被吐槽的。用户明明之前说过的事情Agent 就是“想不起来”。排查下来通常是两个原因要么是写入的时候就没存进去要么是召回的时候检索策略有问题。写入的问题好查在记忆写入的地方打日志看每次抽取有没有产生新的记忆条目。召回的问题稍微麻烦一点需要看检索的 query 和返回的结果是否匹配。我常用的改进方法是用模型来做召回排序先粗筛出十条候选再让模型从中选出最相关的三条。虽然多了一次模型调用但准确率提升很明显。记忆过期的问题前面提过了核心是加时间戳和置信度。但还有一个细节有些记忆是永久有效的有些是有时效的。比如“用户的名字叫张三”是永久有效的“用户明天要去杭州”是时效性的。我在存储的时候会给每条记忆打一个type标签时效性的记忆在过期后自动降权。6.4 多 Agent 协作的故障排查多 Agent 系统出问题的时候最难的是定位是哪个环节出了错。我的做法是在共享黑板上记录每个 Agent 的输入和输出以及每次状态转换的时间戳。出问题的时候按时间线捋一遍很快就能找到是哪个 Agent 的产出不符合预期。另一个实用技巧是给每个 Agent 的输出加一个置信度字段。Agent 在输出的时候自己评估一下“我对这个结果有多确定”下游 Agent 看到置信度低的输入时会更加谨慎必要时可以要求上游重新生成。7. 我实际项目中的一些经验体会跑通第一个完整 Agent 项目之后我回头看知乎那门课发现课程里讲的东西确实都在用只是当时没有实战场景很多点体会不深。比如 Function Calling 的工具描述课程里就一句话带过但实际开发中这是最影响效果的因素之一。MCP 课程里可能只是提了个概念但真正接上 Playwright MCP 让 Agent 操作浏览器的时候那种“原来这么简单”的感觉是很强烈的。记忆管理这块课程里讲的分层思路是对的但具体怎么分层、每层存什么、什么时候写什么时候读这些细节都得自己在项目中摸索。我现在的做法是工作记忆保留最近五轮、会话摘要每三轮更新一次、长期记忆按需检索这套参数是在实际项目中反复调整出来的不一定最优但在我做的几个场景里都跑得比较稳。多 Agent 协作我现在的态度是“能不用就不用”。单 Agent 加多工具能解决的问题不要拆成多 Agent。多 Agent 带来的通信开销和故障排查成本是实实在在的只有在任务确实需要不同专业视角反复交互的时候才值得上多 Agent。最后分享一个我觉得很实用的小技巧在 Agent 的系统提示里加一段“自我检查”指令。让模型在每次调用工具之前先在心里过一遍“这个工具是不是当前最合适的选择”“参数是不是都填对了”。这段指令不产生额外输出但能明显降低工具调用出错的概率。我实测下来加了这段之后工具调用的准确率大概能提升一成左右成本几乎为零。