作者: Jinkun
时间: 2026年4月
项目地址: github.com/yaoziyaoguai/my-first-agent
前置阅读:
- 上篇 从零构建 AI Agent (一):Context Engineering 与 Harness Engineering 实战手记
- 中篇 从零构建 AI Agent (二):AI Agent 工程化进阶:从能用到好用的六次架构演进
- 《一篇50年前的经典论文,让我看懂了今天 AI Agent 的记忆设计》
一、计划赶不上变化
写完第二篇博客时,我列了一个清晰的"下一步":
1. 持续漂移检测(cron 定时健康检查)
2. Agent 自审代码
3. 部署到阿里云服务器
这个计划看起来很合理——先完善监控,再提升智能,最后部署。但当我真正开始用自己的 Agent 做日常工作时,发现完全不是这回事。
第一个痛点:Agent 不认识我。 每次启动都是一个全新的对话。它不知道我叫什么、用什么技术栈、有什么偏好。上一次我们讨论了半天的决策,下一次全忘了。
第二个痛点:Agent 不知道什么时候该停。 让它"检查一下项目代码",它就无限循环地 ls -la → read_file → ls -la,永远觉得自己"还没看完"。
第三个痛点:中断后一切归零。 执行到一半我按了 Ctrl+C,重新启动后之前的进度全部丢失,只能从头再来。
第四个痛点:只能全量覆盖文件。 想改一行 CSS,Agent 必须重写整个文件,改着改着接口全变了。
这四个痛点没有一个在我的原始计划里。它们全部来自实际使用——这恰好印证了 Harness Engineering 的 Steering Loop:你不可能预先设计出完美的系统,只能在使用中发现问题,然后迭代。
于是我调整了方向:
原计划:漂移检测 → 自审代码 → 部署
实际路径:记忆系统 → 任务规划 → 断点恢复 → 局部编辑 → 一系列意外的改进
二、记忆系统:让 Agent 认识我
从 Tulving 到工程实现
在设计记忆系统之前,我读了一篇 1972 年的认知心理学论文——Endel Tulving 的《Episodic and Semantic Memory》。这篇论文提出的记忆分类框架,在 50 年后的今天几乎可以直接映射到 AI Agent 的记忆设计上(详见我的另一篇博客)。
三层记忆的对应关系:
| Tulving 的分类 | Agent 中的实现 | 存储位置 |
|---|---|---|
| 情景记忆(Episodic) | 发生过的关键事件 | memory/episodes/*.jsonl |
| 语义记忆(Semantic) | 用户偏好、项目知识 | memory/profile.json |
| 程序性记忆(Procedural) | 行为规则和操作流程 | memory/rules/*.md |
用一个具体例子来理解三者的关系:
情景记忆:"2026-04-10 用户让 Agent 读 .env,API Key 进入了 messages"
↓ 提炼
语义记忆:".env 文件包含敏感信息"
↓ 内化
程序性记忆:"读文件前检查敏感文件模式,匹配则直接拒绝"
信息的流向是:事件 → 知识 → 行为。
存储设计:不用框架,纯 JSON 文件
考虑过向量数据库和各种记忆框架(Mem0、Zep、LangMem),但作为个人使用的本地 Agent,复杂度太高。最终选择了最简单的方案:
memory/
├── profile.json ← 语义记忆
│ {
│ "user": {"name": "Jinkun", "tech_stack": [...]},
│ "preferences": {"code_style": "偏好类型注解", ...},
│ "knowledge": [{"fact": "...", "confidence": "high", "reason": "..."}],
│ "projects": {"my-first-agent": {...}}
│ }
│
├── episodes/ ← 情景记忆(每天一个文件,保留20天)
│ └── 2026-04-10.jsonl
│
└── rules/ ← 程序性记忆(每个行为模式一个文件)
├── tool_usage.md
├── file_and_code.md
├── high_risk_ops.md
└── ...
召回时机的设计
一个关键的设计决策:什么时候把记忆塞进上下文?
最初的设计是每次用户输入后都搜索情景记忆。但讨论后发现这不对——语义记忆(知识)已经在启动时加载到了 system prompt 里,它是常驻的。大多数时候用户提问需要的是知识,不是历史事件。
情景记忆什么时候有用?只有用户主动引用过去的时候——"上次我们做的那个东西"、"之前那个 bug 怎么解决的"。
最终的召回设计:
Agent 启动时:
→ profile.json + rules/*.md → 注入 system prompt(常驻)
用户输入后:
→ 通常不搜索情景记忆
→ 只有用户提到"上次""之前""继续"等词时才搜索 episodes
Session 结束时:
→ LLM 从对话中提取新的记忆 → 更新三个存储
System Prompt 的瘦身
有了记忆系统后,system prompt 发生了一次质的变化。之前我写了一个将近 3000 字符的 system prompt,包含了身份、原则、工具规则、文件处理规则、高风险操作规则、输出格式、沟通风格……
问题是:这些规则硬编码在 config.py 里,每改一条都要改代码。而且它在每次 API 调用时都完整发送,占用上下文窗口。
改进方案:system prompt 只保留核心身份(~300 字符),所有详细规则拆到 memory/rules/ 下。
# 之前:3000+ 字符的 system prompt
SYSTEM_PROMPT = """你是一个通用智能 Agent...
[工具使用规则]...
[文件处理规则]...
[高风险操作规则]...
[输出组织规则]...
[沟通风格]...
"""
# 之后:~300 字符的核心身份
SYSTEM_PROMPT = """你是一个通用智能 Agent。
你的职责是理解用户的真实目标...
核心原则:
1. 目标导向...
2. 先判断后行动...
3. 真实可靠...
"""
# 详细规则通过 build_memory_prompt() 从 memory/rules/ 加载
好处是三重的:
- 可维护性:改一条规则只改一个 .md 文件,不用改 config.py
- 可扩展性:Agent 以后可以自己往 rules/ 里写新规则
- 为选择性加载留了口子:虽然现在全量加载,但数据已经分开了
记忆提取的三次迭代
记忆系统最难的不是存储和加载,而是从对话中提取什么。
第一版提取 prompt:
提取规则:
1. episodes:关键事件
2. knowledge:新发现的通用知识或事实
3. rules:新的行为规则或教训
结果:提取了 25 条 knowledge,但全是项目实现细节——"fetch_url 超时时间为 15 秒"、"ruff 输出截断至 2000 字符"。这些跟我这个人没有任何关系,只是 Agent 在执行任务时的输出。
问题分析: prompt 说"新发现的通用知识或事实",模型就把它看到的所有"事实"都记下来了。它不知道我们真正想记住的是关于用户的信息。
第二版提取 prompt——核心改动:
2. knowledge:从对话中提取关于用户的长期偏好和决策习惯,重点关注:
- 用户主动表达的观点和偏好("我觉得..."、"我更偏向...")
- 用户在多个方案中的选择,以及选择的理由
- 用户对建议的接受或拒绝
- 用户反复提及或深入追问的话题
- 用户有明显情绪反应的观点和决策
- 尽量将具体行为抽象为习惯或模式
不要提取:
- 模型单方面的输出内容(代码细节、工具返回值、具体参数)
- 没有经过用户确认或选择的信息
- 一次性的任务细节
关键转变:从"提取事实"变成了"提取关于用户的洞察"。而且加了反面约束——这是之前在工具描述调优中学到的技巧:告诉模型"不要做什么"往往比"要做什么"更有效。
第二版结果: knowledge 从 25 条变成了 0 条。太严格了?其实不是——测试那次对话里我确实没有表达过偏好。从 25 条垃圾到 0 条空白是进步,因为错误的记忆会误导 Agent,空的记忆只是还没积累。
还给 knowledge 加了一个 reason 字段——记录"为什么提取这条"。比如:
{
"fact": "用户偏好先理解概念再动手",
"confidence": "high",
"reason": "用户多次在学习新功能时要求先解释为什么再写代码"
}
这样回顾记忆时能知道每条知识的来源,也帮助模型更好地理解这条偏好的适用场景。
三、任务规划:让 Agent 知道什么时候该停
无限循环的根因
之前让 Agent "检查一下项目代码",它的行为是:
ls -la → read_file(main.py) → read_file(config.py) → ls -la → read_file(core.py) → ls -la → ...
87 条消息,一直循环。工具调用次数限制(MAX_TOOL_CALLS_PER_TURN = 20)最终拦住了它,但这只是治标。
根本原因是:Agent 每一步只做一个决策——"下一步干什么"。它没有一个全局的计划说"我总共要做这五件事,做完就停"。
规划机制的设计
方案是在执行前多调一次模型,专门做任务判断:
用户输入
↓
generate_plan(独立 API 调用)→ 判断需要几步 → 生成计划
↓
展示计划给用户确认
↓
用户确认后,计划注入 messages → Agent 按计划执行
↓
所有步骤完成 → 自动停止
谁来判断"需不需要规划"?
最初让模型判断 needs_plan: true/false,但效果不好。用户说"帮我看一下 agent 文件夹下的代码,一步一步做",模型判断"不需要计划"——因为它没有把"文件夹"推理成"多个文件"。
后来改成让模型估算步骤数(steps_estimate),代码判断 > 1 就需要计划。但本质上还是推理型控制,模型的判断不完全可靠。
最终的方案是三层结合:
1. /plan 指令 → 用户主动触发,最可靠
2. 自动判断 → 兜底,不准也没关系
3. rules 里的行为规则 → Agent 执行工具前简要说明意图
关于 /plan 指令的灵感来自 Claude Code——让用户主动告诉 Agent "这个任务请先规划",而不是让模型猜。计算型触发比推理型判断可靠得多。
规划器的独立性——一个重要的工程教训
最初给规划器传了最近 6 条对话历史,希望它能理解上下文。结果全部报错——json.loads 收到空字符串。
调试后发现:模型看到历史消息后,角色混乱了。 system prompt 说"输出 JSON",但历史消息里的对话模式更强烈,模型延续了聊天角色而不是结构化输出。
[DEBUG] plan raw response: '**收到确认**,按新计划执行深度代码分析...'
它输出的是聊天内容,不是 JSON。
教训:需要模型输出结构化数据的独立调用,不要混入对话历史。 对话上下文和结构化输出是两种不同的"模式",混在一起模型会混淆。
去掉历史消息后,规划器只看当前输入,反而工作得更好。而且发现一个有趣的现象——即使规划器生成了一个"模糊的计划"(因为看不到上下文),主对话模型能结合完整历史把细节补上:
规划器(无历史):
步骤1:分析当前上下文
步骤2:生成报告
→ 模糊但结构正确
主对话模型(有完整历史):
看到"分析当前上下文" → 知道是指 agent/ 目录
→ 填入具体细节执行
模糊的计划 + 完整的历史 = 有效的执行。 计划提供结构(几步做完就停),历史提供内容(具体做什么)。
四、断点恢复:中断不再是灾难
设计决策:保存什么?
断点恢复需要在中断时保存状态,下次启动时恢复。核心问题是保存什么。
方案一:追踪步骤号。 记录"当前执行到第几步"。但怎么知道"这一步完成了"?让模型输出 [STEP N] 标记?这是推理型控制,模型可能不遵守。
方案二:保存 messages 历史。 不追踪步骤,保存已有的对话记录。恢复时模型自己判断做到哪了——这正是模型擅长的。
选了方案二。配合一个小优化:保存时截断 tool_result 里的大块内容,但加上"[此步骤已成功完成]"的标记,防止恢复后模型以为"上次没读完"而重做。
三种退出方式
quit → 正常退出:提取记忆 + 保存快照 + 保留断点
单次 Ctrl+C → 暂停任务:保存断点 + 给用户选择(继续/放弃/退出)
连续两次 Ctrl+C → 强制退出:保存快照 + 保留断点
quit 是"我做完了",Ctrl+C 是"我先走了,回头继续"。两者的行为不同——quit 时提取记忆(对话完整),Ctrl+C 时不提取记忆(对话不完整,提取质量差)。
Checkpoint 的生命周期
计划确认时 → save_checkpoint(创建)
执行过程中 → checkpoint 保持不变
Ctrl+C 中断时 → save_checkpoint(更新最新 messages)
任务正常完成时 → clear_checkpoint(清除)
下次启动时 → load_checkpoint → 提示用户是否继续
五、意外的演进:使用中"长出来"的改进
以下几个改进都不在原始计划里,全部来自实际使用中遇到的问题。
edit_file:局部编辑
之前只有 write_file(全量覆盖)。想改一行代码,Agent 必须重写整个文件,经常把不该改的也改了。
解决方案出奇地简单——实现一个查找替换工具:
@register_tool(name="edit_file", ...)
def edit_file(path, old, new):
content = file_path.read_text()
if content.count(old) > 1:
return "匹配到多处,请提供更精确的内容"
updated = content.replace(old, new, 1)
file_path.write_text(updated)
模型先用 read_file 看到完整内容,然后指定"把什么改成什么"。不需要行号,不需要 diff,不需要 copy。核心逻辑就是 str.replace。
smart shell 确认
痛点是每次执行 shell 命令都要按 y,即使是 ls -la 这种无害命令。把 shell 命令分成两类:
READONLY_COMMANDS = {"ls", "cat", "find", "grep", "wc", "head", "tail", "pwd", ...}
def _check_shell_confirmation(tool_input):
command = tool_input.get("command", "").strip()
first_word = command.split()[0]
if first_word in READONLY_COMMANDS:
return False # 自动执行
return True # 需确认
一个担心是 cat .env 也是只读命令但应该被拦截。但不需要在确认层处理——run_shell 函数内部已经有敏感文件检查了。确认机制和安全检查是两层独立的控制:
确认机制:决定"要不要问用户" → cat 是只读 → 不问
安全检查:决定"允不允许执行" → .env 是敏感文件 → 拒绝
防循环检测
Agent 反复调用同一个工具、传同样的参数。计算型检测:
call_signature = f"{tool_name}:{json.dumps(tool_input, sort_keys=True)}"
recent_calls.append(call_signature)
if len(recent_calls) >= 3 and len(set(recent_calls[-3:])) == 1:
result = "检测到重复调用,请基于已有信息继续下一步"
连续拒绝强制停止
用户连续按 n,Agent 不断换方式尝试达到同一个目标。tool_result 里的"请停下来"是推理型控制,模型可能不遵守。加了计算型兜底:
consecutive_rejections += 1
if consecutive_rejections >= 3:
return "用户连续拒绝了多次操作,任务已停止。"
直接 return 跳出 while 循环,模型没有机会继续。这是计算型控制兜底推理型控制的又一个实例。
上下文窗口优化
Kimi-k2.5 提供 256K token 的上下文窗口(约 50 万字符),但原来的压缩配置太激进:
# 之前:只用了 10% 就开始压缩
MAX_MESSAGES = 10
MAX_MESSAGE_CHARS = 50000
# 之后:用到 80% 才压缩
MAX_MESSAGES = 100
MAX_MESSAGE_CHARS = 400000
之前 Agent 反复读同一个文件的原因之一就是压缩太早——有用的历史被压缩掉了,模型"忘记"自己读过什么。调大阈值后,模型在大多数任务里都能看到完整的历史。
用户拒绝的反馈机制
之前用户按 n,tool_result 只说"用户拒绝了此操作"。模型不知道为什么被拒绝,就换一种方式重试。
改成三种返回值:
# confirm_tool_call 的返回值:
True → 同意执行
False → 拒绝,无理由
"文字" → 拒绝,带反馈意见
# core.py 里的处理:
if approved is True:
result = execute_tool(...)
elif isinstance(approved, str):
result = f"用户拒绝了此操作,反馈如下:{approved}"
else:
result = "用户拒绝了此操作。请停下来询问用户需要什么调整。"
六、两个重要的认知转变
从"按计划推进"到"在使用中发现方向"
写完 Blog 2 时,我的计划是"漂移检测 → 自审代码 → 部署"。实际走的路是"记忆 → 规划 → 断点 → 编辑工具 → 一系列意外的改进"。没有一个重大改进是原始计划里预见到的。
这不是计划失败,这就是 Steering Loop 的本质。Harness 原文说"Harness 是长出来的,不是设计出来的"——记忆系统、任务规划、断点恢复,全都是在使用中遇到痛点后才去做的。
启示:不要试图在开始之前想清楚所有事情。先跑起来,用起来,痛点会自己告诉你下一步该做什么。
从"控制 Agent 的输出"到"与 Agent 合作"
前两篇的主线是 Harness——怎么约束 Agent、怎么防止它做错事。本篇的主线变了——怎么让 Agent 更懂我、怎么让交互更顺畅。
记忆系统按某种角度也是Harness子集,它也是 Context Engineering——让模型看到更好的信息。任务规划是 Harness(Guide)和 Context Engineering(注入计划到上下文)的结合。断点恢复是纯工程。
这说明随着 Agent 能力的成熟,关注点会从"防御"转向"协作"。前期你担心的是"Agent 会不会搞坏我的文件",后期你关心的是"Agent 能不能更高效地帮我做事"。
七、踩坑清单
| 问题 | 根因 | 解决方案 |
|---|---|---|
| 记忆提取全是项目细节 | 提取 prompt 太宽泛 | 重写 prompt 聚焦用户偏好 + 加反面约束 |
| 规划器返回聊天内容而非 JSON | 传了历史消息导致角色混乱 | 规划调用不带历史,只传当前输入 |
| 模型判断"不需要计划"但实际需要 | 推理型判断不可靠 | 加 /plan 指令让用户主动触发 |
| ls -la 也要确认很烦 | shell 全部 always 确认 | 只读命令白名单自动执行 |
| 按 n 后 Agent 换方式继续重试 | 推理型"请停下来"被忽略 | 连续拒绝 3 次代码层面强制 return |
| config.py 自己导入自己 | 编辑时误加了 from config import | 删除循环导入 |
| 压缩太早导致模型"失忆" | 256K 窗口只用了 10% | 阈值调到 80% |
| 模糊计划反而能工作 | 规划器无历史但主对话有 | 两者各司其职:结构 + 内容 |
八、当前完整架构
my-first-agent/
│
├── config.py ← 配置中心(精简版 System Prompt ~300字符)
├── main.py ← 入口 + 启动检查 + 断点恢复 + Ctrl+C 处理
├── .pre-commit-hook.sh ← Git 提交前检查
│
├── agent/
│ ├── core.py ← Agent Loop(防循环 + 连续拒绝停止)
│ ├── tool_registry.py ← 注册中心(装饰器 + 钩子)
│ ├── planner.py ← 任务规划(/plan 指令 + 自动判断)
│ ├── checkpoint.py ← 断点恢复
│ │
│ ├── tools/ ← 7 个插件化工具
│ │ ├── calc.py ← calculate(AST 安全版)
│ │ ├── file_ops.py ← read_file + read_file_lines
│ │ ├── write.py ← write_file + pre/post 钩子
│ │ ├── edit.py ← edit_file(局部编辑)
│ │ ├── shell.py ← run_shell(smart 确认 + 四层防护)
│ │ ├── web.py ← fetch_url
│ │ └── outline.py ← 多格式结构提取
│ │
│ ├── security.py ← 通用安全(确认、敏感文件、脚本检测)
│ ├── context.py ← 上下文压缩(256K 窗口优化)
│ ├── review.py ← 跨模型审查 + 自动重试
│ ├── checks.py ← 运行时 linter 检查
│ ├── health_check.py ← 启动健康检查
│ ├── memory.py ← 三层记忆系统
│ └── logger.py ← 日志 + 快照
│
├── memory/
│ ├── profile.json ← 语义记忆
│ ├── episodes/ ← 情景记忆(保留 20 天)
│ └── rules/ ← 程序性记忆(8 个行为规则文件)
│
└── workspace/ ← Agent 工作区
九、核心认知(续)
计划型控制 vs 计算型控制的反复印证
每次遇到行为问题,解决路径都是同一个模式:
推理型控制(告诉模型"请不要...")→ 模型可能不遵守
↓
计算型控制(代码层面强制)→ 可靠但可能太粗暴
↓
两者结合 → 推理型先引导,计算型兜底
连续拒绝停止、工具调用上限、防循环检测——全都是这个模式。
Context Engineering 的细节决定一切
记忆提取 prompt 改了几个词,效果从"25 条垃圾"变成"精准提取用户偏好"。工具描述加一句反面约束,误触发就消失了。规划器去掉历史消息,JSON 解析就正常了。
你注入到上下文里的每一个字都会影响模型的行为。 这不是夸张——一个 [此步骤已成功完成] 的标记就能决定 Agent 恢复时是从断点继续还是从头重做。
模糊的分工反而更好
规划器不需要很精确——它提供结构("分几步做完"),主对话模型填充内容("具体做什么")。记忆系统不需要完美——它提供背景,模型在具体场景中灵活运用。
Agent 开发中最常见的错误是过度设计——试图在代码层面控制每一个细节。但模型本身就擅长理解上下文和灵活决策,很多时候你只需要给它正确的信息和合理的边界。
使用者的痛点是最好的路标
原始计划里的"漂移检测"和"部署到云端"不是不重要,而是还没到时候。真正的优先级应该由使用体验决定——你天天按 yes 的时候不会想着"我应该做漂移检测",你只想着"能不能不按这个 yes"。
十、下一步
- 真实场景验证:用 Agent 做一些真实的工作(修改博客代码、分析数据、读技术文档),在使用中发现下一批痛点
- 记忆系统的长期效果:观察随着对话积累,knowledge 和 rules 的质量是否真的在提升
- 性能优化:长对话场景下的 token 消耗、API 调用延迟
- 部署到阿里云:当本地功能足够成熟时
但我已经不再为下一步写详细的计划了。先用起来,痛点会告诉我该做什么。
本文所有代码均为作者亲手编写和调试。学习过程中与 Claude 协作,采用苏格拉底式教学。如果你也在做 Agent 开发,欢迎交流:wangjinkun333.me