最近我把一个跨平台个人助理 Agent 的模拟项目从零跑通了一遍,目标挺朴素:让 AI 自己查天气、看待办、记偏好、还能操作一下浏览器。真做起来才发现,模型能力只是地基,真正决定这个 Agent“能不能干成事”的,是 ReAct 循环怎么设计、LangChain Agent 怎么封装工具、任务要不要拆成子代理、记忆该写到哪里。这也让我想把这段时间踩过的坑整理成一份实战向的指南,围绕 LangChain Agent、ReAct 思路、Copilot-Agent 模式、Computer Use 和记忆机制展开。
这不是一篇堆概念的文章,更像我的项目复盘。适合刚开始接触 Agent 开发、想从单纯调 API 进阶到“让模型自己决定调用什么工具”,或者对某通用型 Agent 产品背后的调度机制和交互设计感兴趣的人。你不需要读过论文,只要会写 Python、用过 LLM 接口,就能跟着走一遍。
1. ReAct:Agent 思考的底层逻辑
1.1 为什么不能只用“Prompt + 工具”
很多同学第一次做 Agent 时,习惯写成固定的流水线:先调用 A 工具,再调用 B 工具,最后把结果拼给 LLM。这在任务路径明确时没问题,比如“查天气再转成文案”。但真实请求往往是不确定的,用户说“我今天适合干什么”,Agent 得先看天气、再看待办、可能还要翻一下日程,执行顺序取决于上一步的结果。
ReAct 解决的就是这个“动态决策”问题。它的核心思想是让模型在推理(Reason)和行动(Act)之间来回切换:先想一步,再动一步,看完结果继续想。整个过程很像人类做事:我看到冰箱空了(Observe),决定去超市(Thought),买了菜回来(Action),再看冰箱满了没有(Observe)。
传统的“Prompt + 工具”是一条直线,ReAct 是一个循环。正是这个循环,让 Agent 有了根据环境反馈修正计划的能力。
1.2 核心循环拆解:Thought - Action - Observation
ReAct 每一轮迭代都围绕五个字段展开:
- Thought:模型解释自己这一步在想什么,为什么要这么做
- Action:决定调用哪个工具
- Action Input:给工具准备的参数
- Observation:工具返回的结果
- Final Answer:确认任务完成后,给用户的最终答复
这五个字段在工程上通常用 JSON 约束模型输出,方便程序解析。我的 System Prompt 里会写死输出格式,要求模型只能返回两种结构:一是“需要调工具”的结构,包含 thought、action、action_input;二是“任务完成”的结构,包含 final_answer。
我在最小实现里用的提示词模板大概是这样的:
text复制你是一个能调用工具的助手。你可以使用以下工具:
- get_weather(city: str): 查询某城市实时天气
- get_todo(user: str): 查询某用户待办
每次回复必须输出 JSON:
1. 需要工具时: {"thought": "你的推理", "action": "工具名", "action_input": "参数"}
2. 已有结论时: {"thought": "你的推理", "final_answer": "给用户的最终回复"}
不要编造工具结果,不要在一次回复中连续调用多个工具。
这个约束非常重要。没有严格输出格式,模型特别容易在中间步骤里夹带聊天内容,导致解析器崩溃。我一开始图省事,让模型自由文本输出,结果十次里有三次解析失败。
1.3 一个最小 ReAct 循环示例
下面这段代码是我在项目里验证循环逻辑用的最小实现,没有加任何框架依赖,方便看清楚 ReAct 的每一步:
python复制import requests
def call_llm(messages):
resp = requests.post("http://127.0.0.1:11434/v1/chat/completions", json={
"model": "local-llm",
"messages": messages,
"temperature": 0
})
return resp.json()["choices"][0]["message"]["content"]
TOOLS = {
"get_weather": {"desc": "查询城市天气", "run": lambda c: f"{c} 多云,24℃"},
"get_todo": {"desc": "查询用户待办", "run": lambda u: "1.写周报 2.预约体检"},
}
SYSTEM_PROMPT = "你是一个能调用工具的助手,按要求输出 JSON。"
def react_agent(user_query):
messages = [
{"role": "system", "content": SYSTEM_PROMPT},
{"role": "user", "content": user_query}
]
for step in range(6):
output = call_llm(messages)
messages.append({"role": "assistant", "content": output})
if "\"final_answer\"" in output:
return output
action = output.split("\"action\": \"")[1].split("\"")[0]
action_input = output.split("\"action_input\": \"")[1].split("\"")[0]
observation = TOOLS[action]["run"](action_input)
messages.append({"role": "user", "content": f"Observation: {observation}"})
return "达到最大步数,终止"
这段代码里的 JSON 解析比较粗糙,生产环境建议用正则或标准 JSON 解析,但核心循环就是这样一个“思考→行动→观察”的闭环。每一步模型都能看到之前所有的思考轨迹和观察结果,所以它不会“失忆”,这一点对多步骤任务特别重要。
我当时跑了一个实际例子:用户问“明天去北京出差,要不要带伞”。模型第一步会调 get_weather 拿北京天气,Observation 返回“小雨”,第二步就会推理出“需要带伞”,然后输出 final_answer。整个过程完全由模型自主决策,不需要我在代码里写 if 判断。
1.4 ReAct 的边界与改进
ReAct 模式最大的问题有两个。
一是步数多了之后,模型容易“跑偏”。到第五步第六步时,它可能开始重复之前做过的动作,或者在思考里反复绕圈。我在循环里加了最大步数限制,超过 6 步直接终止,宁可让用户重新描述,也不让 Agent 无限循环烧 token。
二是对输出格式非常敏感。换一个模型,可能 JSON 格式就变了,或者模型开始“说人话”而不是“输出 JSON”。后来我改用更稳的方式:优先用模型的函数调用能力(Function Calling),让它原生返回结构化参数,而不是靠提示词硬压 JSON。但如果你的模型不支持函数调用,ReAct 的 JSON 解析方式仍然是最通用的方案。
注意:ReAct 的每一步都要把整段历史重新发给模型,token 消耗比普通问答高很多。生产环境建议设单次任务步数上限,并为每一步的 Observation 做截断,避免工具返回超长文本把上下文撑爆。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. LangChain Agent:把循环封装成工程
2.1 Agent 四件套:模型、工具、提示词、执行器
如果每个 Agent 都自己写“解析 JSON、调工具、循环”的代码,项目很快就变成一团乱麻。LangChain 的价值在于把 ReAct 这套循环抽象成了标准组件,我自己重新梳理后,发现核心就是四样东西:
- 模型(LLM):负责推理决策
- 工具(Tools):Agent 能调用的外部能力,比如查天气、查数据库、发邮件
- 提示词(Prompt):告诉模型有哪些工具、输出什么格式
- 执行器(AgentExecutor):负责跑循环、管理中间步骤、处理解析错误
理解这四样东西就够了。LangChain 内部再花哨,本质都是在帮我把 1.3 里的循环工程化。
2.2 用 LangChain 快速搭一个 ReAct Agent
对应前面的天气和待办例子,在 LangChain 里的写法是这样:
python复制from langchain.agents import create_react_agent, AgentExecutor
from langchain.tools import tool
@tool
def get_weather(city: str) -> str:
"""查询指定城市的实时天气,入参为城市中文名"""
return f"{city}:多云,24℃"
@tool
def get_todo(user: str) -> str:
"""读取指定用户的待办事项,入参为用户名"""
return "1.写周报 2.预约体检 3.订机票"
tools = [get_weather, get_todo]
agent = create_react_agent(llm, tools, prompt)
executor = AgentExecutor(agent=agent, tools=tools,
verbose=True,
handle_parsing_errors=True,
max_iterations=6)
result = executor.invoke({"input": "今天北京天气如何?顺便看下我的待办"})
print(result["output"])
这段代码里,@tool 装饰器把普通函数变成可被 Agent 调用的工具,函数的 docstring 会成为模型理解工具用途的关键信息。handle_parsing_errors=True 是我特别推荐开的,模型偶尔会输出非法 JSON,打开这个参数后执行器会把解析错误反馈给模型,让它自我纠正,而不是直接崩溃。
2.3 工具描述怎么写,决定了 Agent 的选型准确率
工具描述是最容易被低估的细节。模型不是靠函数名理解工具的,它靠的是 docstring 和参数签名。我踩过一个很典型的坑:一开始把 get_weather 的描述写成“获取天气”,结果用户问“上海冷不冷”时,模型有时候不调工具,直接凭训练数据回答“上海冬天比较冷”。后来我把描述改成“查询指定城市当天的实时气温和天气状况,适合回答关于天气冷暖、是否下雨、是否需要带伞的问题”,准确率立刻上来了。
所以工具描述要回答三个问题:这个工具是干什么的、什么时候应该调用它、传入参数应该长什么样。描述写得越具体,模型的选型就越准。我还习惯在描述里加一句“当用户询问 X 类问题时必须使用此工具”,用强约束避免模型跳过工具。
2.4 从 Demo 到生产,LangChain 还需要补什么
用 LangChain 搭 Demo 很快,但直接上生产会暴露几个短板。
第一,执行器的中间步骤默认都存在内存里,一旦重启全部丢失。第二,日志不够结构化,出了问题很难回放是哪一步决策错了。第三,Agent 调工具出错时,默认错误信息很粗糙,需要自己封装“工具异常时返回友好提示”的机制。
我的做法是在工具函数内部统一做 try-except,任何异常都返回“工具执行失败:具体原因”,让模型根据这个 Observation 重新决策。这比让执行器直接报错要有效得多,因为模型有机会换一种方式完成任务,而不是整个流程中断。
经验:工具一定要做成“无论成功失败都有返回值”的函数。让模型在 Observation 阶段自己判断结果是否可用,这也是 ReAct 循环的真正价值。
3. Copilot-Agent 模式:从单循环到子代理编排
3.1 为什么单循环撑不起复杂系统
当工具数量超过十个,Agent 的决策质量会明显下降。模型面对一长串工具列表,经常选错工具,或者反复在几个工具之间试探。我在项目里把工具加到十二个以后,明显感觉到模型“选择困难”了:用户说一句“帮我安排出差”,它一会儿调天气工具,一会儿调日历工具,一会儿又去查机票,来回烧了好几轮 token,还不一定把事办完。
这时候就需要引入编排思想:把一个大 Agent 拆成一个“主管理”和多个“子代理”,每个子代理只负责一个领域,工具列表保持精简,决策压力就分散了。
3.2 核心思想:编排器 + 子代理,子代理也是工具
业界把这种模式叫作 Copilot-Agent 模式,核心有两条:
第一,有一个 Orchestrator(编排器)负责理解用户意图、拆解任务、把请求分发给对应的子代理。第二,子代理对外伪装成普通工具,编排器根本不需要关心子代理内部用了几步 ReAct,它只需要知道“调用这个工具能拿到什么结果”。
从代码结构上看,其实就是把 AgentExecutor 包成 tool:
python复制@tool
def todo_sub_agent(query: str) -> str:
"""处理所有日程、待办、会议相关的请求,入参是用户的完整问题"""
return todo_executor.invoke({"input": query})["output"]
@tool
def weather_sub_agent(query: str) -> str:
"""处理所有天气查询相关的请求,入参是用户的完整问题"""
return weather_executor.invoke({"input": query})["output"]
# 编排器只面对两个工具
orchestrator = create_react_agent(llm, [todo_sub_agent, weather_sub_agent], ORCHESTRATOR_PROMPT)
orchestrator_executor = AgentExecutor(agent=orchestrator, tools=[todo_sub_agent, weather_sub_agent])
这个设计的精妙之处在于,每个子代理内部可以继续拆孙代理,形成递归的树状结构。我测试过三层嵌套:主编排器 → 出差规划子代理 → 酒店预订孙代理,每层只暴露两三个工具给上层,模型决策准确率高了很多。
3.3 编排器提示词的三个要点
编排器的系统提示词和普通 Agent 不一样,它更像一个“部门经理”。我在实验后总结出三个必备要点:
- 不自己干活:编排器不直接调用底层工具,只负责分发给子代理
- 先拆后调:遇到复杂请求,先思考要先问哪个子代理,必要时连续问多个
- 信息不足就反问:子代理返回的信息不够回答用户时,编排器应该向用户追问,而不是编造
我踩过一个反复出现的坑:编排器偶尔会跳过子代理,自己凭训练数据回答用户。解决办法是在提示词里加一句“你无法直接获取实时信息,任何具体数据都必须来自子代理工具,禁止根据常识作答”。这句话能堵住绝大多数“幻觉”情况。
3.4 这种模式的适用范围与代价
子代理编排不是免费的。每一个子代理都是一套完整的 Agent 循环,调用链变长,延迟明显增加。我实测从用户提问到最终回答,子代理模式比单循环平均多 3 到 5 秒。另外,子代理内部的错误会被吞掉一层,排查问题时需要把每一层的 verbose 都打开,看完整调用链。
所以我的选型建议是:工具少于 5 个用单 ReAct,5 到 15 个先按领域分组拆 3 到 5 个子代理,超过 15 个才考虑多级嵌套。不要为了架构好看而拆层,每拆一层都是延迟和排查成本的叠加。
4. 某通用型 Agent 产品热潮背后的产品机制
4.1 过程透明化:让用户看到 Agent“正在干什么”
最近某个爆火的通用型 Agent 产品让整个行业重新思考了一个问题:Agent 和聊天机器人在交互上到底有什么本质区别?我的观察是,最大的区别不在模型能力,而在“过程透明化”。
传统聊天机器人是黑盒:用户输入,等待,看到回答。而那个产品借鉴的交互方式叫 Progressive Disclosure,渐进式披露——Agent 每执行一步,就把“正在写代码搜索文件、正在分析结果、正在生成报告”这类中间过程实时展示给用户。用户不再焦虑地干等,而是在看一个“数字员工”工作。
这种设计对开发者的启发很大。我在自己的项目里加了类似的“过程事件流”:每次 Agent 调用工具,就往前端推送一条事件,包含当前 Thought 和工具名。用户看到“正在查询天气”时,对延迟的容忍度明显提高。实现上只需要在 AgentExecutor 里挂一个回调函数,把中间步骤流出去。
4.2 异步任务与背后的多代理解调度
那一类产品还有一个反常识的设计:任务不是“提问→等待→回答”的同步模式,而是“接受任务→后台执行→完成后通知”的异步模式。用户甚至可以先关掉页面,过一段时间再回来看结果。
技术上要达到这个效果,后台一定有一个任务队列和一套多代理调度系统。我拆解过它的行为模式:一个大任务进来,先由一个规划代理分解成若干子任务,再把子任务分发给不同 worker 代理并行执行,最后汇总成报告。这和上一节说的子代理编排是同一个思想,只是多了一层“异步化”和“并行化”。
如果你的项目需要支持异步任务,最简单的方案是引入消息队列:主服务收到任务后立即返回 task_id,后台 worker 消费队列执行 Agent 循环,完成后把结果写入数据库,前端轮询 task_id 获取进度。这一步从架构上把 Agent 从“在线服务”变成了“任务系统”,吞吐量完全不一样。
4.3 Agent 产品给我们的三个可借鉴点
抛开具体产品,我从这个热潮里提炼出三个值得开发者抄进自己项目的设计:
第一,过程可视化。不管 Agent 内部是不是雷,给用户一个“正在做什么”的进度感,体验会好一个档次。
第二,任务可追踪。给每个任务生成唯一 ID,用户随时可以回溯这个任务执行了哪些步骤,这是信任感的基础。
第三,控制在用户手里。Agent 执行到关键动作(比如删除文件、支付)时,应该暂停等待用户确认。那种产品在潜在高风险动作前同样会“卡一下”,这不是性能问题,是安全边界设计。
这三点不依赖任何特定模型,纯产品层设计,但决定了用户愿不愿意把真事交给 Agent 干。
5. Computer Use:让 Agent 学会“用电脑”
5.1 技术链路:从截屏到点击的循环
Computer Use 是让 Agent 直接操作图形界面完成任务的路线,比如自动填表、操作网页、整理文件。它和前面最大的不同是:前面调用的是 API 工具,这里调用的是“鼠标键盘”。
实现一条完整链路需要四个环节:感知(拿到屏幕/DOM 状态)、决策(模型决定下一步点什么)、执行(模拟点击输入)、验证(观察结果是否发生变化)。
桌面端的感知手段主要是截屏,决策主要靠多模态模型。流程就是“截图 → 模型看图找目标控件坐标 → 执行鼠标操作 → 再截图确认”,本质还是一个 ReAct 循环,只是 Observation 从文本变成了图像。
5.2 一个简单的桌面控制 Demo
下面这个例子用桌面自动化库做了个最小闭环,模型负责看图找坐标,程序负责执行点击:
python复制import time
import pyautogui
def computer_use_step(instruction):
# 1. 截取当前屏幕
screenshot = pyautogui.screenshot()
screenshot.save("screen.png")
# 2. 多模态模型从图中找出目标控件,返回坐标
box = vision_model.find_click_target("screen.png", instruction)
if box is None:
return "未找到目标控件,请检查屏幕状态"
# 3. 模拟点击
x, y = box.center_x, box.center_y
pyautogui.click(x, y)
# 4. 等待页面响应,再次截图供下一步观察
time.sleep(2)
return pyautogui.screenshot()
这个循环跑起来不难,但稳定性堪忧。我实测同一个操作在不同分辨率屏幕上,坐标完全不一样;页面稍微异步加载慢一点,截图时按钮还没渲染出来,模型就误判“控件不存在”。纯坐标方案只适合演示,不适合生产。
5.3 为什么浏览器方案比桌面方案稳得多
如果你要做的场景主要是网页操作,强烈建议用浏览器扩展方案,而不是桌面坐标模拟。浏览器方案的核心是:不把页面“当图看”,而是把页面结构直接拿给模型看。
具体做法是:浏览器扩展的 content script 读取当前页面的 DOM 树和无障碍树,压缩成一份带编号的结构化文本,模型看到的不是像素,而是“按钮 12:登录、输入框 7:用户名”。模型决策输出的是“点击编号 12”,程序直接执行 DOM 节点的 click() 方法。
这套方案有三个明显优势:不依赖分辨率、不依赖元素位置、可以读取文本内容。坐标方案需要反复校正偏移量,DOM 方案天然免疫这些低级问题。代价是只能操作网页,不能操作桌面软件。
我的建议是:做办公场景的自动化优先选浏览器方案;只有必须操作桌面原生软件时才上坐标方案,而且一定要加“截图对比验证”来提高可靠性。
5.4 安全护栏:Computer Use 的底线设计
让 Agent 拥有鼠标键盘权限,等于把电脑控制权交了出去,安全护栏是必须的,不是可选的。我在实验时给自己的系统定了三条硬规则:
第一,危险动作确认。涉及删除文件、发送消息、提交订单、支付类动作,Agent 只能“准备执行”,必须经过用户点击确认后才真正执行。
第二,步骤上限与看门狗。整个操作循环最多 10 步,超过立即停止,防止 Agent 在错误的循环里反复点击。
第三,只读优先。默认 Agent 只有读取和执行非破坏性操作的权限,写操作一律单独开白名单。
这三条规则我写在了一个独立的 guard 模块里,所有模拟点击都要经过它校验。没有这些护栏,Computer Use 做出来的 Demo 越强,出事风险越大。
6. 记忆机制:Agent 的灵魂
6.1 四种记忆类型怎么分工
Agent 没有记忆,每次任务都是“失忆”重启,用户稍微多说几句就露馅。我跑完整个项目后深刻体会到,记忆不是面子工程,是 Agent 能不能持续服务的关键。
我把记忆分成四层来设计:
- 工作记忆:当前对话上下文窗口里的内容,比如用户正在输入的问题、上一步工具返回的结果
- 短期记忆:最近几轮对话的摘要,用于理解当前会话的来龙去脉
- 长期记忆:跨会话沉淀的用户事实,比如用户的名字、常去城市、偏好偏好
- 程序记忆:模型提示词里携带的工具描述和技能说明,相当于 Agent 的“肌肉记忆”
工程上最容易混淆的是短期记忆和长期记忆。我的区分标准是:短期记忆存“这次会话发生过什么”,长期记忆存“这个用户一直是什么样的人”。
6.2 向量库 + 摘要:一种务实的长期记忆方案
长期记忆的存储我采用了“向量库 + 文本摘要”的组合。核心设计很简单:
python复制class MemoryManager:
def __init__(self, llm, vectorstore):
self.short_term = []
self.summary = ""
self.vectorstore = vectorstore
def save_fact(self, user_id, content):
# 只保存明确的用户事实,不保存闲聊内容
self.vectorstore.add_texts(
texts=[content],
metadatas=[{"user_id": user_id, "time": time.time()}]
)
def load_relevant(self, user_id, query):
docs = self.vectorstore.similarity_search(
query,
k=3,
filter={"user_id": user_id}
)
return [doc.page_content for doc in docs] + [self.summary]
def summarize_short_term(self):
self.summary = summarize(self.short_term)
self.short_term = []
向量库负责检索相关记忆,摘要负责保留“概况”。这样做的好处是:用户问“我一般几点睡觉”时,可以检索到之前散落的对话记录;用户问“我之前说过什么重要的”,摘要可以接住。
6.3 记忆写入与检索策略:不是所有对话都值得记
记忆系统最容易犯的错是“什么都存”。把所有对话塞进向量库,不仅浪费存储,检索时还会把大量噪声带进上下文,反而干扰模型判断。
我的写入策略只有两条:第一,用户主动陈述的事实必须存,比如“我下周去深圳出差”;第二,模型经过推理确认的偏好可以存,比如多次提到晚上效率高。其余闲聊一概不写。
检索策略上,我把记忆分成“强相关”和“弱相关”:强相关就是和当前问题语义相似的记忆,直接进上下文;弱相关只作为摘要附在系统提示词末尾,给模型一个背景感,但不占主要 token。这样既保留了记忆,又避免了检索结果太多把上下文冲垮。
6.4 记忆工程的几个深坑
第一,权限隔离不能省。我一开始做多用户测试时,出现过用户 A 的记忆被检索给用户 B 的情况,原因就是向量库查询忘了加 user_id 过滤。记忆是隐私数据,过滤条件必须写死。
第二,记忆会过期。用户说“我常去上海”,半年后可能早不去了。我给每条记忆加了时间戳,检索时做时间衰减,太久远的记忆权重自动降低。
第三,模型容易“记忆中毒”。如果上下文里混入了互相矛盾的记忆,模型会无所适从。我遇到过一次:用户先说过“喜欢安静”,后来又说过“喜欢热闹”,模型开始两头讨好。解决办法是检索时不仅返回记忆内容,还返回记忆时间,让模型知道哪个更新。
7. 常见问题与排查实录
7.1 问题速查表
下面这张表是我和身边几个同样在做 Agent 的朋友高频碰到的问题集合,可以直接对照排查:
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 模型不输出 JSON 格式 | 提示词约束力不够 | 改用函数调用能力,或加输出解析器兜底 |
| 工具调用参数总是错 | 工具描述没写清楚参数含义 | 重写 docstring,示例化参数格式 |
| Agent 陷入重复调用循环 | 最大步数没限制,或工具异常没反馈 | 设置 max_iterations,工具内 try-except 返回友好提示 |
| 上下文被工具结果撑爆 | Observation 文本过长 | 对工具返回做截断,只保留关键信息 |
| 用户记忆串了 | 向量库查询缺少 user_id 过滤 | 所有检索强制带用户过滤条件 |
| 子代理互相调用成环 | 编排器误选了子代理工具 | 在编排器提示词里禁止调用同层子代理 |
| 点击坐标偏差 | 截屏像素坐标和实际屏幕缩放不一致 | 改用浏览器 DOM 方案,或修正缩放比 |
| 任务执行一半丢失 | 流程无状态,重启即失忆 | 引入任务持久化,每步写入数据库 |
7.2 排查实录:模型反复调用同一个工具
有一次模型的循环一直停留在“查天气”,调用完一次结果还不够,继续调用第二次,像是“停不下来”。我打开 verbose 日志才发现,模型每一步都在调用同一个工具,但 Observation 只有一个模糊的“多云”,没有足够信息回答用户“要不要带伞”。它不是傻,是觉得信息不够,又不知道去哪里拿补充信息。
排查思路很清楚:一看完整调用链,二看每一步的 Observation 信息量。解决办法是给“查询天气”工具增加一个“查询未来三天逐小时天气预报”的兄弟工具,让模型在信息不足时有第二条路可走。这比单纯调大模型参数靠谱。
7.3 排查实录:子代理把问题吞了
用子代理编排后,我遇到一个隐蔽问题:用户问“帮我规划下周出差”,天气子代理返回“北京晴”,待办子代理返回“周三有会议”,但最后编排器只回复了一段“已为你查询天气和待办”,完全没有给出任何建议。原因是编排器把子代理当成了“信息获取工具”,拿完数据后忘了自己还要“综合推理产出答案”。
我在编排器的提示词里加了一条硬规则:拿到所有子代理结果后,必须基于这些结果生成完整的答复,禁止只罗列工具返回原文。这之后输出质量才恢复正常。这类问题在单循环里不存在,是编排模式特有的坑,排查时要特别注意。
调试技巧:把 AgentExecutor 的 verbose 打开,再把每个子代理的日志输出到一个带时间戳的独立文件。别人问“你的 Agent 为什么这么回答”,你直接甩一条完整调用链,比用嘴解释一百遍都清楚。
7.4 排查实录:记忆检索把无关内容带进上下文
向量检索看起来简单,实际用起来很容易带偏模型。有一次用户问“帮我查下网球场”,系统从记忆里检索出一堆关于“运动鞋”“健身房”的旧对话,模型受干扰后回答里开始推荐运动装备。
原因是向量相似度只看了“语义接近”,没看“意图边界”。我补救的方法是:检索时同时用关键词做硬匹配,只有同时命中“网球”相关关键词的记忆才进上下文;语义相似只作为排序依据,不作为入选依据。这套“关键词过滤 + 向量排序”的组合,在项目里比纯向量检索稳定得多。
8. 技术选型与演进路线参考
8.1 从固定流程到 Computer Use 的选型逻辑
用完整条链路后,我整理了一条选型路线,给不同阶段的团队参考:
| 场景复杂度 | 推荐方案 | 核心关注点 |
|---|---|---|
| 固定问答流程 | 普通 Chain,顺序调用工具 | 稳定性,几乎不需要 Agent |
| 需要动态决策任务 | 单 ReAct Agent | 工具数量控制在 5 个以下 |
| 工具多但领域集中 | Copilot-Agent 模式,编排器 + 子代理 | 各子代理的职责边界要清晰 |
| 跨领域、跨会话 | 子代理 + 记忆系统 | 记忆写入和权限隔离 |
| 需要操作界面 | 浏览器 DOM 优先,桌面模拟兜底 | 安全护栏,动作确认 |
这里要特别强调一句:不是所有项目都需要上全套技术。我见过不少团队一上来就搞“多代理 + 记忆 + GUI 操作”,结果调试成本高到项目烂尾。Agent 开发最忌过度设计,先确认“这个任务真的需要模型自己决定调用哪个工具吗”,再决定要不要用 ReAct,这是首要原则。
8.2 各层技术如何配合在完整项目里落地
如果完整落地一个“能办事、能记住、能操作用户偏好”的 Agent,我的技术栈大致是:底层用 LangChain 做 Agent 框架,按领域拆成 3 到 5 个子代理;子代理之间用编排器串联,编排器本身也是一个 ReAct Agent;长期记忆用向量库存事实,短期用对话摘要;如果需要操作网页,单独跑一个浏览器扩展模块,和其他 API 工具并列注册。
最核心的经验是:把这五层分层管理,每一层的 prompt、工具列表、记忆逻辑都要独立文件,不要写在一个巨型脚本里。我踩过的最大教训就是“全部逻辑堆在一个文件”,改一个工具描述要全局 review,排查问题无从下手。分层后,每层的替换、升级、测试都变得可控。
最后想说的几点
这个项目跑完,我最深的一个体会是:Agent 开发看起来门槛很低,写个 demo 半小时的事,真正难的是稳定性、可控性和记忆的准确性。ReAct 循环能跑通,但步数一多就飘;LangChain 很方便,但工具描述质量决定了成败;子代理编排很优雅,但每一层都在增加延迟和排查难度;记忆很有价值,但要克制写入、严格隔离。
如果让我重来一遍,我会调整顺序:先不碰子代理和 Computer Use,用最朴素的单 ReAct 把两三个工具打磨到 95% 稳定;再加记忆,把多轮对话和跨会话体验做扎实;最后才扩展子代理和 GUI 操作。每一步都吃透了再往上叠,远比一开始就追求“全家桶”要扎实。希望这篇指南能帮你少走一点我走过的弯路。
