说实话,这两年被“个人 AI”这个概念折腾过很多次。早期我做过几个看起来还挺聪明的聊天助手,能记住用户上次聊到哪儿、记得住偏好、甚至能复述自己的行为逻辑。但有一个问题我一直绕不开:它永远只是“记得”,从来不会“干”。你让它“把这三份资料里的需求点拉出来,整理成一张对照表,再用 500 字写个推荐结论”,它能答得头头是道,却给不出任何可以拿去启用的交付物。
这就是我理解的“记忆型 AI”的真相:它擅长把对话延续下去,但不擅长把任务闭环掉。所以当“AI Agent”这个词开始流行、大家都说智能体能真正干活的时,我还是持保留态度的,直到我们花几天时间,把一个个人 Agent 从纯对话形态推到“真正开工”的阶段,我才确认了一件事——记忆从来不是能力,围绕任务的控制流才是。
这篇文章不聊概念,只讲我们实际怎么拆解、怎么搭框架、怎么踩坑又怎么把 Agent 推到能产出结果的状态。给正在做 agent 开发、或者准备从“聊天机器人”往“干活智能体”转的同学一个能直接参考的路线。
1. 为什么“记忆型 AI”是一条死胡同
1.1 “能记住”和“能干活”中间隔着什么
先说个最直观的对比。一个记忆型 AI,核心能力是检索和对话,它的链路通常是这样:用户输入 → 拼接历史记录 → 检索相关记忆或知识 → 生成回答。这条链路决定了它的天花板——它做得再好,也只是“回答得更好”,而不是“任务完成了”。
而一个能真正开工的 Agent,链路完全不同:接收任务 → 拆解步骤 → 调用工具 → 观察结果 → 修正方案 → 产出最终交付物。注意,每一步之间不是靠一段 prompt 拼起来的,而是靠程序逻辑在循环里驱动的。模型只负责“想”和“调”,真正的“做”要交给文件操作、接口请求、代码执行这些实在的工具。
我见过很多朋友把 Agent 做成一个超长 prompt 的聊天玩具:告诉模型“你可以用工具”,然后就没有然后了。问题是,没有可执行的工具层和循环控制,模型再强也只能在文本里打转。它给你输出一段“我已经帮你整理好了”的格式,但文件系统里什么都没有多出来。
1.2 重新定义“真正开工”的验收标准
我们这次动手前,先给项目定了一个目标:Agent 必须在无人逐字盯着的情况下,独立完成包含至少两个工具调用的任务,并且交付一个可检查、可使用的产物。
举个例子,我给它布置过这么一个任务:“从这几篇技术文章里提取关键结论,写一份 800 字左右的行研速递,输出成 Markdown 文件,并且标注每个结论对应的原文链接。”这个任务放到聊天 AI 上,它能写出一篇漂亮的作文,但链接很可能是编的,文章也未必真去读了;但我们的 Agent 要做到的是:真正去请求文章页面、把正文截取下来、做要点提取、再落盘成一个文件。
我把这叫作“可验收的产出”。一句回答不是产出,一份落盘的文档、一张由程序生成的表格、一段真正执行过的脚本,才算。这是我们评测 Agent 是否“开工”的第一条铁律,后面所有架构和调试,都是围绕这条规则来的。
2. Agent 的整体架构:不是一个模型,而是一条可控的流水线
2.1 我们选择了“计划-执行-验证”循环,而不是一把梭
现在 Agent 框架很多,有跑图结构的、有多智能体协作的、有完全让模型自由发挥的。我们评估了一圈,最后没有选择一上来就上很重的多 Agent 框架,而是采用了一个极其朴素的“计划-执行-验证”循环,和经典的 ReAct 模式类似,但加了两层我们自认为更重要的控制:停止条件和人工确认节点。
为什么这么选?因为 Agent 最怕的不是模型不聪明,而是失控。完全自由发挥的 Agent 哪怕成功率高,一旦出错,你很难定位是“模型想错了”还是“工具用错了”还是“中间状态丢了”。而分步骤的循环架构,每一步都能打印日志,哪一步出问题,一眼就能看到。当时团队里有人提议直接用现成的 agent 框架和编排平台,省时间,但我们的需求很窄:就是一个能在一个目录里查资料、写文件、跑脚本的小助手,不需要跨系统复杂协作。这种情况下,自己维护一个简单的循环反而更稳。
2.2 模块拆成五块,各干各的活
整个 Agent 在代码上拆成了五个模块,彼此之间通过消息传递,而不是共享全局状态:
- 任务规划器:负责把用户的自然语言任务转成一个有顺序的步骤列表,这个列表会展示在界面上,用户可以临时删改。
- 工具注册表:所有 Agent 能调用的能力,比如读文件、写文件、请求网页、执行 Python 代码,统一按规范注册在这里,模型只能访问注册过的工具。
- 执行循环:程序层面的核心,负责调度:把模型输出的“工具调用意图”翻译成真实函数调用,再把结果作为观察反馈回去,循环往复。
- 记忆管理器:分两层,短期记忆保存当前任务上下文,长期记忆通过向量检索,读取历史任务中的事实性结论。
- 安全与熔断:包括最大迭代次数、单次执行超时、危险操作确认,以及最终产物的落盘检查。
这套划分不是什么原创设计,几乎是所有能开工的 Agent 的标配。但很多人忽略的是:模块之间必须解耦,尤其是工具执行结果,不能直接被模型乱改。我们所有工具返回值都走一层统一的序列化格式,哪怕工具执行失败,也返回标准错误结构,这样模型能理解失败原因并作出下一步判断,而不是一头雾水地瞎解释。
2.3 技术选型:模型负责判断,代码负责兜底
大模型选型上,我们用的是支持函数调用(function calling)的模型,因为工具调用用结构化参数比让模型自己编 JSON 要稳定得多。同时任务规划、摘要这类对“深度思考”需求高的环节,用推理能力强一点的模型;纯格式化的环节,用便宜快速的模型。混合路由能在成本和质量之间取一个平衡点。
代码层我们直接用 Python 写主循环,没有引入太重的分布式框架。向量记忆用一个轻量的本地库加文件存储就够用了——真正干活时,你的长时记忆没那么频繁,没必要一开始就上大规模服务。这里有两个热词我需要点一下:harness和skill。harness 指的是套在模型外面那一层控制壳,也就是我们的执行循环和安全熔断;skill 则是可复用的能力包,比如“解析 PDF 并生成摘要”的完整流程。对于个人 Agent,先定义清楚 harness,再往里面逐步加 skill,是最稳妥的落地顺序——我见过太多项目先写一堆 skill,结果主循环还不稳定,最后什么都跑不通。
3. 核心环节实现:工具、循环、记忆和边界
3.1 先定义工具:接口越“笨”越好用
Agent 能不能干活,首先看工具定义得清不清楚。我们有一个工具注册装饰器,每个函数都附带一个 JSON Schema 描述,包括参数类型、必填项、使用说明,甚至给了示例值。下面是我们在项目里实际用过的简化版:
TOOLS = {} def tool(name, description, parameters: dict): def decorator(func): TOOLS[name] = { "function": func, "description": description, "parameters": parameters, } return func return decorator @tool( name="read_file", description="读取本地文件的文本内容,适用于 .txt .md .py 等文本格式。", parameters={ "type": "object", "properties": { "path": {"type": "string", "description": "文件的相对路径或绝对路径"} }, "required": ["path"] } ) def read_file(path: str): try: with open(path, "r", encoding="utf-8") as f: return {"status": "ok", "content": f.read()[:5000]} except Exception as e: return {"status": "error", "message": str(e)}这里有个细节很关键:工具描述里必须包含“什么时候用”和“什么时候不该用”。比如 read_file 只适合读文本文件,如果模型遇到 PDF 记录时准备调用它,就会失败,这时候描述里如果能补一句“无法解析 PDF,请使用 parse_pdf”,模型就会自动转向正确工具。
我给你的建议是:第一版别急着做二十个工具,先做三个——读文件、写文件、请求网页。把这三个工具的准确率打磨到 95% 以上,再谈扩展。工具多了以后,模型经常会选错,原因不是模型笨,而是你的工具描述不够“明显”。
3.2 执行循环:给 Agent 装上刹车
主循环是整个 Agent 的心脏,它看起来很简单,但里面全是细节。我们核心代码大概 60 行,逻辑是:把系统提示、用户任务、历史消息拼成上下文 → 请求模型 → 判断模型输出是“调用工具”还是“给出最终回答” → 如果是工具调用,执行函数并捕获异常 → 把结果追加到上下文 → 回到第一步。直到模型给出 final 标记,或达到最大轮次。
简化版如下:
messages = [ {"role": "system", "content": SYSTEM_PROMPT}, {"role": "user", "content": task} ] for step in range(MAX_STEPS): # 例如 15 resp = llm.chat_with_tools(messages=messages, tools=tool_schemas) if resp.type == "final": final_answer = resp.text break if resp.type == "tool_call": tool_result = safe_execute(resp.tool_name, resp.arguments) messages.append({"role": "assistant", "content": f"调用 {resp.tool_name},结果是: {tool_result}"}) continue else: final_answer = "已达到最大执行步数,任务被中止,请简化问题后重试。"注意那个 else 分支,这相当于刹车。Agent 跑飞是常态,尤其面对开放任务,模型可能一会儿要总结、一会儿要重读文件、一会儿又要写新的脚本。我们在实测中发现,很多中途跑飞的任务,真正的解法是让模型“停下来问人”,所以我们给系统提示里加了一条强制规则:如果连续三次工具返回值没有带来新信息,必须调用 ask_user 工具向用户澄清。
这里补一个我们内部使用的小技巧:每轮工具调用结束,我们会把“工具名 + 关键返回值的长度 + 一个哈希指纹”追加到一个运行摘要里,而不是把完整长文本全部塞回上下文。这样模型每次都能快速感知“这轮到底有没有新东西”,上下文也不会爆炸。
3.3 记忆管理器:短记忆防丢、长记忆防串味
“agent记忆”是我们这次重点研究的问题之一。我踩过最大的坑是:把所有对话历史一股脑塞给模型。看起来信息全,实际上模型会被大量无关细节干扰,反而抓不住当前任务的焦点。
我们的做法是分两层:
短期记忆保留当前任务的步骤和中间结果,但做了压缩。每轮工具执行后,只保留结构化摘要,比如“第 2 步:读取 pdf 成功,获取 3 条核心结论”,完整内容存到临时目录,等摘要判定需要时再读原文。
长期记忆走向量检索,任务结束后,Agent 会把“事实性结论”写入记忆库,每条记忆是一段短文本,附上来源和写入时间。下一次遇到相似任务,检索器只拉相关度最高的 3 条记忆加入提示词。这个设计和 RAG 很像,但关键区别在于我们写入的不是聊天记录,而是“结论”本身,避免让模型把历史对话当成事实依据。
“记忆会串味”这件事,我们在第一天就领教过——它把前一个任务里读到的数据,当成下一个任务的输入来用,导致最终文件里出现两个完全不相关的数字。后来我们在每条长期记忆上都加了“来源字段”,并且要求模型在引用时明确说明“该结论来自哪次任务的哪个文件”,才压住了这个毛病。
3.4 安全边界:不是不信任,而是必须可回滚
让 Agent 真正干活,意味着给它权限。危险不在于模型“坏”,而在于我们没设置护栏,一旦模型犯错就会路径依赖地放大。所以不管用哪个 agent 框架,安全层都必须包含:
- 工具级别的权限细分。Entity 比如代码执行,我们默认不允许;需要手动开启,并且所有执行都发生在临时沙箱目录里。
- 文件操作不支持覆盖已有文件,除非任务文本里明确包含“覆盖”二字,这也算简单的语义确认。
- 每次写出的最终交付物,都会在落盘前自动生成一个
.bak备份文件,即使 Agent 写出了错误内容,人也能轻松恢复。
我不建议把安全做成打扰式确认,每步都问用户会把人烦死。我们只对“删除文件、发送请求到外部、执行任意代码、覆盖旧文件”这类高风险操作做确认,其余自动执行。实测下来,这个策略在效率和安全感之间是平衡的。
4. 几天里踩过的坑:排查实录与避坑指南
4.1 无限循环:模型一直在“分析”,就是不交付
首个版本上线不到十分钟,Agent 就陷入了一个经典循环:它不断调用 read_file 重读同一个文件,每一次都说“为了确认细节”,然后继续读下一遍。问题根源在于停止条件太弱,模型没有“我已经拿到足够信息”的判定标准。
我们的修复方案有两层:一是加了“连续三次工具调用结果与上次相同则强制停止”的规则;二是在系统提示里明确要求——只要目标结论已经能支撑最终交付物,就不允许继续追加搜索或阅读,直接输出 final。这两层下来,循环出现概率从 30% 压到了 3% 以下。
4.2 工具调用参数花式报错
这是 agent 开发里最高频的问题。模型会传错类型、漏传必填字段、甚至把工具名拼错。我们的解法是:不在模型侧解决,而在程序侧解决。safe_execute 内部做了三层校验:工具名必须严格匹配注册表;参数按 schema 自动做类型转换;不合法时返回一个明确错误信息,告诉模型“path 参数缺失,请参考工具描述中的示例”,让模型自行修正再试。
这个策略比重试整个对话要好太多。模型接到具体错误提示后,下一步修正的成功率大约在 80% 左右。另外,我们在工具 schema 的 description 里故意放着 1 个示例值,效果比单纯写参数说明可靠不少。
4.3 上下文越滚越长,成本直线上涨
前面提过摘要压缩,这里再补一组真实数据:一个包含 5 次工具调用的任务,如果不做压缩,上下文里会滚进大约 12000 token 的工具返回值;做了摘要压缩之后,只需保留 2500 token。这也是为什么我们每轮工具返回只保留结论和来源链接,具体正文让模型临时按需再读。
文本类工具也全部加上了 max_length 参数,防止一次性吞进整个网页或 PDF 的全部文本。实测下来,token 费用下降了约一半,而且模型并没有因为信息少了而变得更笨——恰恰相反,它的专注度反而更高了。
4.4 模型幻觉:它说“已经完成任务”,但产物根本没写
最诡异的一个 bug 是:模型在最终回答里写了“已完成,文件保存为 output.md”,但我在目录里怎么都找不到文件。查日志发现,它压根没有调用 write_file,而是把“输出内容”直接放在了 final 回答里。这时候我们才意识到,所谓“完成”,模型眼里只是“生成了文本”,而不是“执行了动作”。
我们最后的解决办法是增加一个终局校验器:当模型声称“任务完成”时,如果任务的原始目标里包含“生成文件”的诉求,程序会强制检查文件是否存在并且非空;不满足就直接把错误抛回给模型,让它重新执行。这一步之后,“假完成”的情况几乎绝迹了。
4.5 评测不能靠感觉,得建一个小型任务集
热词里有个词叫agent evals,我非常认同。我们花了半天整理了 20 个测试任务,覆盖了信息提取、多文件汇总、代码生成、格式转换几类典型场景。每次对系统做调整,都跑一遍这个任务集,记录成功率、平均步骤数、token 消耗三个指标。
这个任务集的价值不在于一次得过且过,而在于持续回归。比如我们后来为了省成本换过一个模型,一看成功率从 95% 跌到了 75%,立刻回滚。没有评测集,这种劣化你很可能两三天后才意识到,到那时排查成本已经很高了。
下面是我们经常参考的“问题现象 → 解决思路”速查表:
| 现象 | 深层原因 | 处理方案 |
|---|---|---|
| 反复读取同一文件不推进 | 停止条件缺失 | 增加“连续无新信息触发询问/结束”规则 |
| 参数反复传错 | 工具 schema 不够明确 | 加示例值,返回具体错误描述让模型自纠错 |
| 上下文越来越长,成本飙升 | 全量保留工具返回 | 做执行摘要,只留结论和来源 |
| 声称完成但产物不存在 | 模型把“文本”当成“动作” | 加终局校验器,检测文件是否落盘 |
| 换模型后效果下降 | 缺少回归测试 | 搭建 20 条固定任务的评测集持续回归 |
4.6 几个容易被忽略的架构取舍
关于agent框架与编排,我再说一句可能得罪人的话:框架只是脚手架,不是银弹。我们初期也尝试过引入比较复杂的编排框架,优点是模块整洁,但随着业务变得具体,原来框架里“帮忙处理的东西”反而变成了黑盒,出问题不好排查。
“skill和agent的区别”也是我们内部经常争论的话题。最后统一的认识是:skill 是一套可以被调用的、偏固定的子流程,比如“整理周报”;而 agent 是一个在复杂环境中动态决策的主控体。个人项目最好先从 skill 堆起,等发现需要动态决策的场景多了,再把它们装进一个 agent 里,而不是反过来一上来就搭一个万能智能体,然后什么都往里塞。
5. 它后来真的开始“开工”了
这套系统真的开始有生产力,是在一个很普通的上午。我给 Agent 布置了一个任务:从产品文档、客服反馈和上周的周会纪要里,提取所有关于“登录流程”的用户问题,按频率排序,生成一份改进建议清单,最后输出成 Markdown 文件。
它先调用读取接口把三份资料逐篇读完,中间我没有任何干预。看到“文档包含 2 个相关段落,现已提取”,然后是“发现第 3 份材料存在重复描述,已合并”,最后它调用写文件工具落盘。整个过程两分钟左右,我打开文件一看,结构清晰,还带上了原文引用位置。
那一刻我挺感慨:它没有做任何惊艳的推理,但它是真的“把活干完了”。这个项目最核心的收获不是某个框架或某个模型,而是一套让 AI 从“说”变成“做”的控制系统。
如果你也想做类似的方向,我的建议是:先选一个自己日常工作里高频、重复、且能被数字化的任务,比如“整理每日情报”“汇总会议结论”“批量清洗表格”,把 Agent 死死圈在这个任务里打磨,不要一开始就追求全领域通用。等单点能力强了,skill 积累多了,再谈个人 Agent 的全面铺开。
根据我这几天的实操经验,一个人 Agent 要真正达到“开工”状态,控制流比模型更重要,工具质量比 prompt 技巧更重要,评测集比灵感更重要。这三条看起来很简单,但每一条都是我们真金白银换回来的教训。