☰
从零搭建个人智能体:agent框架、harness与记忆系统的踩坑复盘
2026/10/2 19:08:54 网站建设 项目流程

把 pi agent 从零搭起来,整个过程跟拿到一套毛坯房差不多:框架给你了,墙是灰的,地是水泥的,水电管线全要自己埋。我前后折腾了小一个月,踩过的坑比卫生间要贴的砖还密。这篇文章不是那种一步步照做的教程,而是把我装修这套 agent 毛坯房时最痛的几个环节拎出来复盘,给准备自己动手搭 agent 或正在被 agent 框架折磨的朋友做个参照。先说清楚它是什么:pi agent 是我给自己做的一个本地优先的个人智能体项目,代号 pi(personal intelligence),能连工具、能调模型、能挂技能,也能跑在低性能设备上。它能解决什么问题?主要是把散落的日常任务——查资料、写周报、整理本地文件、定时提醒——统一收口到一个可以长期用、可以随时改的 agent 里。适合谁看?想自己搞 agent 但不想直接用全家桶、愿意花时间理解底层的人;如果你只想开箱即用,这篇帮不了你太多。

1. 项目定位与整体设计:为什么偏要选“毛坯房”思路

1.1 想象中的“精装交付”和现实差距

最早我差点就走“精装房”路线:找一个现成的 agent 平台,注册账号、点点配置、填几个 API key,看起来一天就能跑起来。但实际用下来发现槽点非常明显。平台把 agent 的基本结构定义得太死了,我想加一个“把网页收藏整理成 Markdown 笔记”的技能,结果工具链被平台锁死,只能写它规定的插件格式,调试起来等于在框架里做适配,不是在写自己的东西。而且数据全在对方云端,我本地文件它碰不到,我想要的“本地优先”核心需求直接被砍掉。

这时候才意识到,所谓精装房,其实是用自由度换便利。而做 agent 这种事,自由度恰恰是最值钱的部分。你早晚要改它的思维链路、要替换模型、要接自己的工具,这些在封闭平台里都是难题。于是决定回到毛坯房:框架自己搭,管线自己埋,每一层都搞明白再往上盖。

1.2 “毛坯房装修”在这个项目里的具体含义

我给自己定的规矩是:不依赖任何一站式 agent 套件,只保留最基础的编程框架,模型接入、工具调用、记忆管理、子任务拆分这些全部自己定义。这就像毛坯房只能提供墙体结构和基础水电点位,其他全部自己设计。

但不是什么都从零写。LLM 调用 SDK 可以复用现成的,HTTP 服务用 FastAPI,任务循环这段核心逻辑自己写。这样做的好处是每换一个模型、每加一个新工具,都知道该动哪一根筋,不担心被框架牵着走。缺点是前期成本明显更高,而且你有大量机会犯低级错误——后文要写的各种坑基本都是这么来的。

1.3 目标拆解与验收标准

动工之前我把“装修验收标准”列了出来,做成清单,后面所有工作都对着这个清单检查:

  • 核心对话链路:用户输入 → 模型决策 → 工具调用 → 结果回填,能在本地完整跑通。
  • 记忆可用:短期对话记忆、长期偏好记忆分开存,重启不丢。
  • 技能可扩展:不用改主程序,加一个新技能只需要新增一个文件。
  • 安全可控:agent 能做的事有权限边界,风险操作要二次确认。
  • 资源可承受:在只有 4GB 内存的迷你主机上也能流畅运行。

这套验收标准在后面帮了大忙。很多时候坑了很久不是因为技术难,而是忘了最初要解决什么问题。

2. 硬装阶段:agent 框架、harness 与底层通道搭建

2.1 框架与 harness 的区别:别选错“施工队”

刚开始接触 agent 开发时,搜索词里总能看见两个词:agent 框架和 agent harness。很多人混着用,但实际施工时区别很大。我的理解是:harness 是脚手架,负责把模型输出、工具调用、任务循环这几块拼起来,让你能看到整个执行过程;框架则是带装修方案的一体化工装,不仅拼起来,还帮你定了房间怎么隔、开关装在哪。

毛坯房逻辑下,我选择先用最少依赖写 harness,而不是一上来就上重型框架。原因有两个:一是重型框架的抽象层级太高,出了问题很难定位,你都不知道是哪一层把消息吞了;二是未来换模型、改协议时,harness 的改动成本低得多。

实际代码结构大体是这个样子,核心循环非常朴素:

messages = load_short_term_memory() while True: response = llm_chat(messages, tools=tool_schema) if response.finish_reason == "tool_calls": for call in response.tool_calls: result = execute_tool(call.name, call.arguments) messages.append({"role": "tool", "tool_call_id": call.id, "content": result}) continue reply = response.content save_short_term_memory(messages + [{"role": "assistant", "content": reply}]) break

这套 harness 足够应付绝大多数单线程任务。真的遇到复杂场景,再往里面加 subagent 和并发控制,不会一开始就背上重担。

2.2 两条主通道:LLM 接入与工具注册

agent 的硬装核心有两条通道:一条是通往大模型的对话通道,一条是工具调用的执行通道。这两条没埋好,后续全是漏水。

对话通道我做了三层设计:统一接口层、模型适配层、错误重试层。统一接口层保证主程序不感知具体模型;模型适配层负责厂商协议转换;错误重试层是我在踩完坑后补的,下面细说。工具注册通道则采用装饰器模式,新增技能只需要暴露函数和参数 schema:

@agent_tool(name="save_note", description="把指定内容保存为Markdown笔记") def save_note(path: str, content: str) -> str: # 实际写入逻辑 return f"saved to {path}"

工具参数说明一定要写清楚,LLM 很依赖参数描述来决定填什么值。描述含糊的后果就是调用时经常传错参数,或者干脆不调用。

2.3 高频翻车:response stream was malformed 的三类现场

我踩到一个特别经典的报错,原话是:

pi error: the response stream was malformed and no response was produced. try again.

第一次看到这行字,我以为是大模型服务端出了问题,后来连续复现才意识到,问题几乎都出在我自己身上。归纳下来有三类:

第一类是流式响应解析太严格。有些厂商的流式输出中间会夹杂空行、注释、或者 SSE 事件里的额外字段,我的解析逻辑一遇到非预期格式就直接抛出异常。处理方式是把解析器改成容错模式:只提取自己关心的字段,忽略其他内容,而不是让解析器去“校验”整个流的合法性。

第二类是超时设置太短。长任务下,模型偶尔会在思考阶段停顿十几秒才输出第一个 token,而我的客户端超时设成了 10 秒,连接被掐断后自然拿不到完整响应。后面把所有超时全部调到 60 秒以上,同时加了心跳,问题明显减少。

第三类是上游模型偶发故障,属于不可控因素。这种场景唯一能做的是做好重试和降级。我用的重试参数是:最多重试 3 次,退避按 1.5 的指数递增,最长等待不超过 10 秒。三张表总结如下:

异常来源典型特征对策
客户端解析过严固定在某段格式特殊内容附近失败解析器改为容错模式
连接超时设置过短固定等一段时间后失败调大超时,增加心跳
上游模型故障随机出现,重试后可能成功指数退避重试,最多 3 次

2.4 并发问题:agent 怎么扛并发,新手最容易想错

相关搜索里频繁出现“ai agent 怎么扛并发”,这问题我也研究过。先说结论:普通个人 agent 项目,建议先别碰并发,老老实实把单路请求跑稳,再去想并发的事。

为什么这么说?agent 和普通 Web API 最大的区别在于它要维护对话状态,相同用户的多轮请求往往共享同一段上下文。如果你无脑加并发,同一上下文的多个任务同时写记忆,会出现记忆错乱、上下文打架,最后行为变得像精神分裂。我在早期实验中用线程池同时跑了两个任务,结果一个任务把另一个任务的中间结果当成了自己的上下文背景,回答彻底跑偏。

真要扛并发,得在“任务”这个层级做隔离,而不是“请求”层级。每个独立任务分配独立的 session,拥有独立的短期记忆和上下文,完成后异步合并结果。同时还要做限流,我用的是令牌桶,单模型通道的消费速率被限制在每分钟 6 次调用,实测对大多数日常场景完全够用。

3. 水电改造:记忆、上下文与 skill 系统设计

3.1 记忆分层:把长期记忆和短期记忆分开

毛坯房的水电改造,对应到 agent 项目就是记忆系统。很多人一开始只用一个 messages 数组,跑几个小时后上下文越滚越长,最后模型直接失忆——开头说的什么都没记住。

我的方案是把记忆拆成三层:短期记忆、长期记忆、工作记忆。

短期记忆是当前对话窗口内的 messages,保存到本地 SQLite,重启对话时恢复。长期记忆是用户偏好和事实类信息,以结构化键值对存储,比如“用户喜欢简洁回复”“用户的工作时间 9:00-18:00”,每次对话结束后由模型抽取关键信息写回。工作记忆是当前任务中的临时状态,任务结束即清除。

三层各自独立存储,互相不污染。这样即使上下文窗口被截断,长期记忆也不会丢。

3.2 上下文窗口卫生:别把记忆当垃圾桶

上下文窗口是 agent 最贵的资源,窗口卫生直接决定回答质量。我用了一个 8k 上下文的模型做测试,一开始把所有历史消息一股脑塞进去,到第 10 轮对话时模型开始答非所问。原因不是模型变笨了,而是早期无关信息把关键指令淹没了。

后来每次请求前做上下文修剪,规则是:

  • 把对话前 2k token 压缩成 300 token 的摘要。
  • 完整保留最近 3 轮对话原文。
  • 长期记忆里与当前任务无关的条目全部剔除。
  • 用户当前指令始终放在最前面,确保模型注意力优先。

压缩后的整体预算大约控制在模型上限的 80%,比如模型支持 8k,我就把所有内容控制在 6.5k 以内,给工具返回结果留出冗余。

3.3 skill 导入与版本管理:从 pi web 导入 skill 踩过的坑

相关搜索里频繁出现“pi web 导入 skill”,这正好戳中我的痛处。skill 我理解成 agent 可复用的能力包,一个 skill 包含一段系统提示词、若干工具定义和触发规则。最初我把 skill 写死在代码里,每次改 skill 都要重启服务,非常痛苦。后来才改成独立文件加载,每个 skill 一个目录:

skills/ web_research/ SKILL.md tools.py requirements.txt note_organizer/ SKILL.md tools.py

SKILL.md 里面写清楚这个 skill 的适用场景、触发条件和关键指令。加载器启动时扫描所有子目录,注册对应的工具,并在系统提示词里注入 skill 的摘要信息。

版本管理也踩了坑。一开始我直接用pip install安装所有 skill 依赖,结果两个 skill 出现依赖冲突,启动报错。后面改成每个 skill 使用独立虚拟环境加 subprocess 隔离执行,冲突问题直接消失。

3.4 subagent 拆分:什么时候该拆、什么时候不该拆

agent 项目里流行用 subagent,也就是主 agent 派生出若干个子 agent 分别处理子任务。但我实际用下来发现,subagent 不是越多越好。

适合拆的场景:一个任务包含多个相互独立的调研方向,比如“对比三款笔记软件的优缺点”,可以拆成三个子 agent 分别调研,再汇总。前提是子任务之间没有状态依赖。

不适合拆的场景:任务链路是严格的串行逻辑,比如 A 的结果直接影响 B 的执行方式。这种拆了反而增加通信开销,而且子 agent 之间只能通过结构化消息交换信息,细节容易丢失。

早期我一股脑把任务全拆成 subagent,结果主 agent 光是等结果就等了十几分钟,还因为某个子 agent 返回格式不规范导致汇总失败。后面我给自己定了一条规矩:单任务执行时间超过 20 秒,且子任务可完全独立时,才允许拆。

4. 软装与安防:权限沙箱、部署形态与实际调试技巧

4.1 给 agent 装防盗门:权限边界与沙箱设计

agent 能调工具,就意味着它有“手”。这手能伸多长,是装修时最需要想清楚的安防问题。我给 agent 的权限做了三层限制:

第一层,文件系统隔离。agent 默认只能访问工作目录,不能读系统目录,更不能碰用户主目录里的敏感文件。读取文件前先做路径规范化,防../../跳目录。

第二层,命令执行隔离。需要执行 shell 命令的技能,全部跑在子进程里,设置独立的用户账号和低权限组,CPU、内存、磁盘配额全部限制。对外网络访问用白名单,只有少数域名允许连接。

第三层,危险操作二次确认。删除文件、覆盖写入、发消息这类特定操作必须经过人工确认才能执行。实现方式是在工具返回结果里插入一个pending_approval标记,主循环遇到这个标记就停下来等用户输入。

这三层下来,即便 agent 被恶意提示词诱导,能造成的破坏也很有限。

4.2 桌面版与服务化:两种部署形态的取舍

项目做到后期需要部署,搜索里经常出现“oh my pi 桌面版”“pi desktop”“hermes agent 桌面版配置”,说明很多人关心桌面形态。我的实践是:服务化和桌面版都做,但它们解决的是不同问题。

服务化部署适合无人值守和远程调用,agent 作为后台服务常驻,通过 HTTP 接口与外部系统对接。桌面版则适合交互场景,用户能直接看到思考过程、手动修改工具结果,适合高频使用场景。我自己在 Windows 上配置桌面版时,最大的坑是环境变量没对:agent 服务读不到 API key 导致反复报认证失败。排查路径是先在终端里确认环境变量能正常输出,再启动桌面客户端,问题瞬间定位。

4.3 场景联动:树莓派、ROS2 与嵌入式设备的接入边界

很多人的 agent 不只是跑在电脑上,还要跟硬件联动。相关搜索里出现“raspberry pi 2040 + oled 0.96”“docker 容器里的 ros2 humble micro-ros agent”,说明这类需求相当普遍。

我在树莓派上也部署过精简版 pi agent,目的是把本地的温湿度传感器数据周期性汇报到 agent 记忆里。硬件接入方式并不复杂,agent 不用直接操作 GPIO,只需要暴露一个本地 HTTP 端口,让树莓派上的采集脚本定时推送数据即可。OLED 屏幕则用来显示 agent 的运行状态,比如当前任务、上下文占用百分比。昂,ROS2 环境如果要接 agent,更建议以 micro-ros agent 作为消息代理,让 agent 通过 ROS2 话题收发信息,而不是直接去改 ROS2 的主程序。

这种联动场景的核心教训是:agent 不要试图直接控制硬件,而要做成消息的消费者和生产者的角色。否则硬件中断和任务循环混在一起,出了问题无从查起。

4.4 调试技巧:从日志反推 agent 意图,而不是猜

agent 调试和传统程序调试完全不一样。传统程序报错有明确堆栈,agent 的“错误”可能只是一个偏离预期的回答或一次多余的调用。这时候必须给 agent 的行动留痕。

我在每轮循环里输出三段日志:thought(模型的思考片段)、action(调用的工具和参数)、observation(工具返回结果)。只要这三个字段完整,任何一个环节出问题都能快速定位。对比下来,过去要花一下午猜“为什么这样回答”,现在看三段日志最多十分钟就搞清楚,基本就是工具返回格式不规范,或者系统提示词某句话导致模型理解偏差。

另外,模型调用失败时不要急着发愁。我遇到“codex 无法发送消息,显示更新 agent 沙盒”这类问题,普遍解法是重启沙盒服务,而不是反复重发消息。沙盒状态和上下文不一致时,重发只会让错误继续滚雪球。

5. 装修验收:一整套实测清单与踩坑速查表

5.1 实测验收清单:我每次改版后都跑一遍

项目改到后期,我总结了一套验收清单,每一版改动都要过一遍,避免修了新 bug 又弄坏旧功能。

  • 单轮对话链路:输入一条指令,观察 thought、action、observation、reply 四个阶段是否完整。
  • 工具调用正确性:让 agent 调用一个需要参数的工具,检查参数是否按 schema 正确填写。
  • 记忆持久化:执行一轮对话后重启服务,确认短期记忆和长期记忆都已恢复。
  • 上下文修剪生效:多轮长对话后,打印实际请求 token 数,确认不超过预算。
  • 权限拦截生效:故意触发一个危险操作,确认需要二次确认。
  • 并发隔离:同时跑两个独立任务,确认双方上下文无串扰。
  • 低资源可用性:在 4GB 内存的机器上连续运行 24 小时,确认无内存泄漏。

5.2 高频故障速查表:都是真金白银换来的

现象根因解法
response stream was malformed解析容错不够/超时太短/上游故障容错解析、调长超时、指数退避重试
上下文越长回答越偏早期信息稀释指令窗口修剪、摘要压缩、关键指令前置
工具调用参数瞎传工具描述或参数描述含糊重写工具定义,例:写明格式和单位
两个 skill 依赖冲突共用全局依赖环境每个 skill 独立虚拟环境执行
并发后行为分裂同一上下文被多任务同时写任务级隔离,独立 session
桌面版启动报认证失败环境变量未正确继承先在终端输出确认变量,再启动

5.3 给新手的装修顺序建议

经历过这一遭以后,我给后来者的核心建议是:先按毛坯房的标准搭一个最小可用的 harness,把单轮对话和工具调用跑通,再考虑记忆、subagent、并发这些高级功能。顺序千万不能反。

反了的典型下场是:框架选得很豪华,功能上了一堆,结果连最基础的工具调用都没跑通,排查问题时每一层都可能是嫌疑犯,根本无从下手。我有个朋友就是这个情况,最后把代码全部推倒重写,只用我最开始的朴素循环,反而两天就上线了。

我个人的体会是:做 agent 项目,最大的坑往往不是技术难点本身,而是“过早追求复杂”。毛坯房的好处就在这:每一根管线你都见过施工过程。等哪天它漏水了,你知道该拆哪块砖。这种底层的掌控感,是用任何精装框架都换不来的。项目后续能扩展的方向也很多,比如把语音输入接到 agent、给 agent 配一个自动写周报的 skill、或者把它做成局域网内多个设备共享的中枢。不过这些都是后话了,先把毛坯房住稳,比什么都强。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询