乐于分享
好东西不私藏

想打造自己的 OpenClaw?从零构建一个可记忆、可扩展、可追踪的本地 AI Agent

想打造自己的 OpenClaw?从零构建一个可记忆、可扩展、可追踪的本地 AI Agent

我受够了那些像“黑盒”一样的 AI 助手:你不知道它到底记住了你多少秘密,更不知道它在调用工具时后台流转了什么数据。这种“盲目信任”的代价,往往是牺牲了透明度与自主权,还有就是有的企业禁止部署使用openclaw。
这就是我折腾出 AgentClaw 的初衷。从 OpenClaw 到 Hermes-Agent,我一直在思考如何让 Agent 真正落地到本地。现在,我整理出了这个基本雏形:代码量精简到核心逻辑仅约 500 行 Python,但它能让你实现真正的“数据主权”——记忆是桌面上随手可改的 Markdown,技能是文件夹里拖进即用的说明书。
跑完这篇教程,你得到的将不再是一个云端租借的对话框,而是一个你完全可控、真正属于自己的 AI 大脑。

先搞清楚我们要做什么
在开始写代码之前,我想先花几分钟解释整个系统的工作方式。这不是废话,架构没搞明白,后面写代码的时候会一直懵。
整个系统分三层:
最上面是前端,用 Next.js 做的对话界面,可选,没有它也能用 API 跑起来。中间是 FastAPI 后端,跑在本地 8002 端口,负责处理请求、读写文件、把事件推给前端。最底下是 Agent 运行时,用 LangChain 构建,这是真正干活的地方。
对大多数人来说,最陌生的可能是 Agent 运行时这一层。简单说就是:它拿到你的问题,决定用哪个工具,调用工具,拿到结果,再决定下一步,直到给出最终回答。整个过程是一个循环,不是一次性的。
整个系统里最关键的设计,是 System Prompt 的构成方式。
每次对话开始,后端会把 6 个本地文件拼成一个完整的 System Prompt 扔给 Agent:
SKILLS_SNAPSHOT.md  ← 告诉 Agent「你会什么」SOUL.md             ← 性格和语气设定IDENTITY.md         ← 自我认知(它知道自己在哪里运行)USER.md             ← 你是谁,你的背景信息AGENTS.md           ← 行为准则,包含技能调用协议MEMORY.md           ← 长期记忆,你告诉它的一切
这 6 个文件全部是本地 Markdown,用 VS Code 或者记事本随时可以打开改。这就是所谓「透明可控」的根基——Agent 的「大脑」不是云端某台服务器里的黑盒,而是你桌面上几个文本文件。

技能系统:它是怎么「学会」新能力的
这里有一个设计让我觉得挺聪明的,叫三级加载机制
很多人第一反应是:给 Agent 加个技能,不就是写个 Python 函数吗?这当然可以,但问题是每加一个功能就要改后端代码,而且如果你有几十个技能,全部描述都塞进 System Prompt,Token 会爆。
AgentClaw 的做法不一样。技能不是函数,是说明书
每个技能是一个文件夹,里面放一个 SKILL.md,描述「什么情况下用这个技能」、「分几步做」、「调哪个工具」。
System Prompt 里只放技能的名字和一句描述(Level 1,每个技能大约 30 个 Token),Agent 看到用户问了个相关问题,先去把这个技能的说明书读进来(Level 2,read_file 工具),然后照着说明书用底层工具执行(Level 3)。
用查天气举例,链路长这样:
用户:「帮我查北京今天天气」       ↓Agent 扫 SKILLS_SNAPSHOT → 找到 get_weather 技能       ↓调用 read_file("get_weather/SKILL.md")       ↓读到:「用 fetch_url 访问 https://wttr.in/Beijing?format=j1,解析 JSON...」       ↓调用 fetch_url(url)       ↓解析数据 → 返回「北京:晴,22°C,湿度 45%
好处是:想加一个新技能,只需要建一个文件夹、写一个 SKILL.md,重启服务,Agent 就自动获得了这个能力。不用改任何后端代码。

6 个内置工具
除了技能系统,Agent 出厂自带 6 个底层工具:
前四个是基础能力,这篇文章都会实现。后两个(RAG 检索和浏览器自动化),先上最简单能运行的AgentClaw。
好,架构清楚了,开始写代码。

环境准备
前置条件不多:Python 3.10 以上,如果你要跑前端再加一个 Node.js 18+。
先建目录结构:
bash
mkdir agentclaw && cd agentclawmkdir -p backend/{api,graph,tools,workspace,skills,memory,sessions}cd backend
安装依赖:
bash
pip install fastapi uvicorn \    langchain langchain-community langchain-experimental langchain-openai \    html2text python-frontmatter python-dotenv pydantic
配置模型。在 backend/ 下建一个 .env
env
# 用 DeepSeek(便宜,工具调用支持不错)OPENAI_BASE_URL=https://api.deepseek.com/v1OPENAI_API_KEY=your-deepseek-keyMODEL_NAME=deepseek-chat# 或者 OpenAI# OPENAI_API_KEY=sk-...# MODEL_NAME=gpt-5.4# 或者直连 Claude(指令遵循最稳定)# ANTHROPIC_API_KEY=sk-ant-...# MODEL_NAME=claude-sonnet-4-6
模型选哪个?开发调试期间用 DeepSeek 就好,够便宜,工具调用也稳。生产环境或者对稳定性要求高的场景,claude-sonnet-4-6 是目前表现最稳的。gpt-5.4 也不错,就是比 DeepSeek 贵不少。
最后目录结构长这样:
agentclaw/└── backend/    ├── .env    ├── app.py    ├── api/    │   └── chat.py    ├── graph/    │   ├── agent.py    │   └── prompt_builder.py    ├── tools/    │   ├── read_file_tool.py    │   ├── fetch_url_tool.py    │   ├── terminal_tool.py    │   └── python_repl_tool.py    ├── workspace/          ← SOUL / IDENTITY / USER / AGENTS.md    ├── memory/             ← MEMORY.md    └── skills/             ← 技能文件夹,一个技能一个子目录

开始写代码
Step 1:初始化工作区文件
先把 6 个 System Prompt 文件建出来。这步很多人会跳过,直接去写代码,结果 Agent 行为乱七八糟,最后才发现是这里没配好。
backend/workspace/AGENTS.md——这是最重要的一个,必须写清楚:
markdown
# AgentClaw 行为准则## 技能调用协议(SKILL PROTOCOL)你拥有一个技能列表(见 SKILLS_SNAPSHOT),其中列出了你可以使用的能力及对应的文件路径。**当用户请求的任务匹配某个技能时,你必须严格遵守以下步骤,不能跳过:**1. 第一步行动永远是调用 `read_file` 工具,读取该技能 location 路径下的 SKILL.md2. 仔细阅读文件中的步骤和示例3. 根据文件指示,使用对应的 Core Tools(terminal / python_repl / fetch_url)执行任务**绝对禁止**:在没有读取 SKILL.md 的情况下,猜测技能的用法或直接执行操作。## 记忆协议当用户告知你重要的个人信息、偏好或背景时,询问是否需要记录。## Canvas 输出协议当用户请求创建图表、网页、仪表盘等可视化内容时,将完整 HTML 包裹在`<openclaw-canvas>` 标签中输出。HTML 必须自包含(所有 CSS/JS 写在文件内)。
为什么要这么强调「必须先读文件」?因为大模型很聪明,它经常「猜」到了怎么做并直接干,绕过了读 SKILL.md 这一步。这在简单任务上没问题,但对于复杂技能(比如操作 Word 文档、调特定 API),绕过说明书几乎必然出错。所以要用命令式语气,把这个约束写死。
其他几个文件简单一些:
markdown
# backend/workspace/SOUL.md你是 AgentClaw,一个运行在用户本地的 AI 助手。你的核心特质:诚实、透明、高效。执行操作时,你会主动告知用户你在做什么,不做任何隐瞒。
markdown
# backend/workspace/IDENTITY.md你由用户部署在本地计算机上运行,所有数据存储在用户自己的文件系统中。你的技能目录:backend/skills/你的长期记忆:backend/memory/MEMORY.md你的配置文件:backend/workspace/
markdown
# backend/workspace/USER.md# 关于用户(请填写你的个人信息,Agent 会据此了解你)姓名:职业:技术栈:常用语言:中文其他偏好:
markdown
# backend/memory/MEMORY.md# 长期记忆(初始为空,Agent 会在你告知重要信息后写入此处)

Step 2:实现 4 个 Core Tools
backend/tools/read_file_tool.py
这是整个技能系统的眼睛,必须加路径限制,否则 Agent 有可能读到你机器上的任意文件。
python
import osfrom langchain_core.tools import toolfrom langchain_community.tools.file_management import ReadFileTool# root_dir 必须用绝对路径,相对路径在某些场景下会出问题_BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))_skills_reader    = ReadFileTool(root_dir=os.path.join(_BASE_DIR, "skills"))_workspace_reader = ReadFileTool(root_dir=os.path.join(_BASE_DIR, "workspace"))_memory_reader    = ReadFileTool(root_dir=os.path.join(_BASE_DIR, "memory"))@tooldefread_file(file_path: str) -> str:"""    读取本地文件内容。用于读取技能定义文件(SKILL.md)或工作区配置。    路径规则:      - 技能文件: "get_weather/SKILL.md"(相对 skills/ 目录)      - 工作区文件: "workspace/AGENTS.md"(加 workspace/ 前缀)      - 记忆文件: "memory/MEMORY.md"(加 memory/ 前缀)    """# 防止路径穿越攻击if".."in file_path:return"Error: 不允许使用 .. 访问上级目录。"try:if file_path.startswith("workspace/"):return _workspace_reader.invoke({"file_path": file_path[len("workspace/"):]})elif file_path.startswith("memory/"):return _memory_reader.invoke({"file_path": file_path[len("memory/"):]})else:return _skills_reader.invoke({"file_path": file_path})except Exception as e:returnf"Error: {str(e)}"
backend/tools/fetch_url_tool.py
原生的 RequestsGetTool 会返回原始 HTML,几千行的 <div> 和 <script> 扔给模型,Token 哗哗地烧。用 html2text 转成 Markdown,能节省 60-70% 的 Token。
python
import html2textfrom langchain_core.tools import toolfrom langchain_community.tools import RequestsGetToolfrom langchain_community.utilities import TextRequestsWrapper_requests = RequestsGetTool(    requests_wrapper=TextRequestsWrapper(),    allow_dangerous_requests=True)_h2t = html2text.HTML2Text()_h2t.ignore_links  = False_h2t.ignore_images = True_h2t.body_width    = 0# 不自动换行@tooldeffetch_url(url: str) -> str:"""    发起 HTTP GET 请求,获取指定 URL 的内容。    HTML 页面会自动转换为 Markdown 格式以节省 Token。    内容限制 3000 字符。    """try:        raw = _requests.invoke({"url": url})if raw and ("<html"in raw[:500].lower() or"<body"in raw[:500].lower()):            result = _h2t.handle(raw)else:            result = rawreturn result[:3000]except Exception as e:returnf"Error fetching {url}{str(e)}"
backend/tools/terminal_tool.py
Shell 工具是把双刃剑,加黑名单是最低限度的安全措施。生产环境建议放到 Docker 容器里跑。
python
import refrom langchain_core.tools import toolfrom langchain_community.tools import ShellTool_shell = ShellTool()# 高危命令正则黑名单_BLACKLIST = [r"rm\s+-rf",r"\bsudo\b",r"chmod\s+777",r">\s*/dev/",r"\bmkfs\b",r"\bdd\b.+if=",r"(curl|wget).+\|\s*(ba)?sh",r":\(\)\{.*\}",          # fork bomb]@tooldefterminal(command: str) -> str:"""    在受限的安全沙箱中执行 Shell 命令。    高危命令(rm -rf、sudo 等)会被拦截。    命令输出限制 2000 字符。    """for pattern in _BLACKLIST:if re.search(pattern, command, re.IGNORECASE):returnf"拒绝执行:命令匹配高危规则 [{pattern}]"try:        result = _shell.invoke({"commands": [command]})return str(result)[:2000]except Exception as e:returnf"Error: {str(e)}"
backend/tools/python_repl_tool.py
这个最简单,直接封装就好:
python
from langchain_core.tools import toolfrom langchain_experimental.tools import PythonREPLTool_repl = PythonREPLTool()@tooldefpython_repl(code: str) -> str:"""    执行 Python 代码并返回结果。    适用于数学计算、数据处理、文件操作、生成图表等任务。    变量在同一会话内保持,可以分步执行。    """try:        result = _repl.invoke({"query": code})return str(result)[:3000]except Exception as e:returnf"Error: {str(e)}"
注意 PythonREPLTool 在 langchain_experimental 包里,忘了装的话启动会报 ImportError

Step 3:技能扫描器(Bootstrap)
这个模块在 Agent 启动时运行,扫描 skills/ 目录,把每个技能的名字和描述提取出来,生成一份「技能菜单」注入 System Prompt。
backend/tools/skills_scanner.py
python
import osimport frontmatter  # pip install python-frontmatterdefgenerate_skills_snapshot(skills_dir: str = None) -> str:"""    扫描技能目录,生成 XML 格式的技能快照。    只提取 name 和 description,不把完整文档都塞进来。    """if skills_dir isNone:        base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))        skills_dir = os.path.join(base, "skills")ifnot os.path.exists(skills_dir):return"<available_skills></available_skills>"    entries = []for skill_name in sorted(os.listdir(skills_dir)):        skill_md = os.path.join(skills_dir, skill_name, "SKILL.md")ifnot os.path.isfile(skill_md):continuetry:            post = frontmatter.load(skill_md)            name        = post.get("name", skill_name)            description = post.get("description""详见技能文件")except Exception:            name        = skill_name            description = "详见技能文件"        entries.append(f"  <skill>\n"f"    <name>{name}</name>\n"f"    <description>{description}</description>\n"f"    <location>./{skill_name}/SKILL.md</location>\n"f"  </skill>"        )    inner = "\n".join(entries) if entries else"  <!-- 暂无技能 -->"returnf"<available_skills>\n{inner}\n</available_skills>"
为什么用 XML 格式而不是 JSON 或纯文本?Claude 系列的模型对 XML 结构的路由准确率明显更高——这是 Anthropic 自己在提示词工程文档里提的,实测确实有差异。

Step 4:System Prompt 构建器
backend/graph/prompt_builder.py
python
import osfrom tools.skills_scanner import generate_skills_snapshotdef_read(path: str, max_chars: int = 20000) -> str:ifnot os.path.exists(path):return""try:with open(path, encoding="utf-8"as f:            content = f.read()if len(content) > max_chars:return content[:max_chars] + "\n\n...[内容过长,已截断]"return contentexcept Exception:return""defbuild_system_prompt() -> str:"""    动态拼接 6 个部分,构建完整 System Prompt。    如果某个文件不存在,对应部分留空,不报错。    """    base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))    parts = [# Part 1: 技能快照(动态生成,不从文件读)"# 你的能力清单\n\n" + generate_skills_snapshot(),# Part 2-6: 工作区文件        _read(os.path.join(base, "workspace""SOUL.md")),        _read(os.path.join(base, "workspace""IDENTITY.md")),        _read(os.path.join(base, "workspace""USER.md")),        _read(os.path.join(base, "workspace""AGENTS.md")),        _read(os.path.join(base, "memory",    "MEMORY.md")),    ]    prompt = "\n\n---\n\n".join(p for p in parts if p.strip())# 总长度保护:超 40000 字符时截断(主要是 MEMORY.md 可能很长)if len(prompt) > 40000:        prompt = prompt[:40000] + "\n\n...[System Prompt 已截断]"return prompt

Step 5:Agent 运行时
backend/graph/agent.py
python
import osfrom dotenv import load_dotenvfrom langchain_openai import ChatOpenAIfrom langchain.agents import create_tool_calling_agent, AgentExecutorfrom langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholderfrom tools.read_file_tool  import read_filefrom tools.fetch_url_tool  import fetch_urlfrom tools.terminal_tool   import terminalfrom tools.python_repl_tool import python_replfrom graph.prompt_builder  import build_system_promptload_dotenv()CORE_TOOLS = [read_file, fetch_url, terminal, python_repl]defcreate_agent_executor() -> AgentExecutor:    llm = ChatOpenAI(        model      = os.getenv("MODEL_NAME""gpt-4o"),        temperature= 0,        streaming  = True,        api_key    = os.getenv("OPENAI_API_KEY"),        base_url   = os.getenv("OPENAI_BASE_URL"orNone,    )    system_prompt = build_system_prompt()    prompt = ChatPromptTemplate.from_messages([        ("system", system_prompt),        MessagesPlaceholder("chat_history", optional=True),        ("human""{input}"),        MessagesPlaceholder("agent_scratchpad"),    ])    agent = create_tool_calling_agent(llm, CORE_TOOLS, prompt)return AgentExecutor(        agent              = agent,        tools              = CORE_TOOLS,        verbose            = True,        max_iterations     = 12,        handle_parsing_errors = True,        return_intermediate_steps = True,    )
max_iterations=12 是个经验值。设太小,复杂任务做到一半会提前停;设太大,出了 bug 的时候 Agent 会在那里循环调工具,Token 烧个不停。12 对大多数任务来说够用。

Step 6:FastAPI 入口与 SSE 流式输出
这是最关键的接口,负责把 Agent 的执行过程实时推送给前端。
backend/app.py
python
import jsonfrom fastapi import FastAPIfrom fastapi.responses import StreamingResponsefrom fastapi.middleware.cors import CORSMiddlewarefrom pydantic import BaseModelfrom graph.agent import create_agent_executorapp = FastAPI(title="AgentClaw", version="1.0.0")app.add_middleware(    CORSMiddleware,    allow_origins=["http://localhost:3000""http://localhost:3001"],    allow_methods=["*"],    allow_headers=["*"],)classChatRequest(BaseModel):    message:    str    session_id: str = "default"# 简单的内存会话历史,重启后清空# 生产环境改成读写 sessions/{id}.json_histories: dict = {}@app.post("/api/chat")asyncdefchat(req: ChatRequest):asyncdefstream():        executor = create_agent_executor()        history  = _histories.get(req.session_id, [])        full_output = ""try:asyncfor event in executor.astream_events(                {"input": req.message, "chat_history": history},                version="v2"            ):                kind = event.get("event""")if kind == "on_chat_model_stream":                    chunk = event["data"]["chunk"].contentif chunk:                        full_output += chunk# 检测是否包含 Canvas HTMLif ("<openclaw-canvas>"in full_outputand"</openclaw-canvas>"in full_output                        ):                            s = full_output.find("<openclaw-canvas>")                            e = full_output.find("</openclaw-canvas>") + len("</openclaw-canvas>")yieldf"event: canvas\ndata: {json.dumps({'html': full_output[s:e]})}\n\n"else:yieldf"event: token\ndata: {json.dumps({'text': chunk})}\n\n"elif kind == "on_tool_start":                    payload = {"tool":  event["name"],"input": str(event["data"].get("input"""))[:300],                    }yieldf"event: tool_start\ndata: {json.dumps(payload)}\n\n"elif kind == "on_tool_end":                    payload = {"tool":   event["name"],"output": str(event["data"].get("output"""))[:300],                    }yieldf"event: tool_end\ndata: {json.dumps(payload)}\n\n"# 更新会话历史(最多保留 20 轮)            history.append({"role""human",     "content": req.message})            history.append({"role""assistant",  "content": full_output})            _histories[req.session_id] = history[-40:]yieldf"event: done\ndata: {json.dumps({'ok'True})}\n\n"except Exception as e:yieldf"event: error\ndata: {json.dumps({'error': str(e)})}\n\n"return StreamingResponse(        stream(),        media_type="text/event-stream",        headers={"Cache-Control":    "no-cache","X-Accel-Buffering""no",   # 关掉 Nginx 缓冲,否则流式输出会卡住        },    )@app.get("/api/health")defhealth():return {"status""ok"}if __name__ == "__main__":import uvicorn    uvicorn.run(app, host="0.0.0.0", port=8002, reload=True)
X-Accel-Buffering: no 这个头如果忘了加,在 Nginx 后面部署时 SSE 会攒够一批数据才推,出来的效果不是流式,是一段一段蹦出来的。本地开发看不出来,部署上去才发现,找半天 bug。

Step 7:创建你的第一个技能
现在写一个天气查询技能,验证整个链路是否跑通。
先建目录:
bash
mkdir -p backend/skills/get_weather
backend/skills/get_weather/SKILL.md
markdown
---name: get_weatherdescription: 获取指定城市的实时天气信息。当用户询问天气、气温、降雨、风速等信息时使用此技能。version: 1.0.0---# 天气查询## Usage当用户询问任何城市或地区的天气、温度、降水情况时,使用此技能。## Steps1. 从用户消息中提取城市名称2. 如果是中文城市名,转换为对应英文(如「北京」→「Beijing」)3. 使用 `fetch_url` 访问以下接口:
https://wttr.in/{城市英文名}?format=j1
4. 从返回的 JSON 中提取:   - `current_condition[0].temp_C`:当前温度(°C)   - `current_condition[0].weatherDesc[0].value`:天气状况   - `current_condition[0].humidity`:湿度(%)   - `current_condition[0].windspeedKmph`:风速(km/h)5. 用自然语言回复用户,格式参考示例## Examples**User**:北京今天天气怎么样?**Assistant**(调用 fetch_url: https://wttr.in/Beijing?format=j1)北京当前天气:晴,气温 22°C,湿度 45%,风速 15 km/h。## Considerations如果 API 返回错误,尝试用不同的英文拼写重试一次不存在的城市名会返回空数据,告知用户并建议换一种写法
现在启动服务:
bash
cd backendpython app.py
看到 Application startup complete 就说明跑起来了。

跑起来之后能看到什么
先用 curl 测一下,不需要任何前端:
bash
curl -X POST http://localhost:8002/api/chat \  -H "Content-Type: application/json" \  -d '{"message": "帮我查一下北京的天气", "session_id": "test"}' \  --no-buffer
如果一切正常,你会看到这样的事件流打出来:
event: tool_startdata: {"tool""read_file""input""get_weather/SKILL.md"}event: tool_enddata: {"tool""read_file""output""---\nname: get_weather\n..."}event: tool_startdata: {"tool""fetch_url""input""https://wttr.in/Beijing?format=j1"}event: tool_enddata: {"tool""fetch_url""output""{\"current_condition\":[{\"temp_C\":\"22\"..."}event: tokendata: {"text""北京当前天气:"}event: tokendata: {"text""晴,气温 22°C,湿度 48%,风速 12 km/h。"}event: donedata: {"ok": true}
注意看 tool_start 那两条。第一个工具调用是 read_file——它先去读了技能说明书。第二个才是 fetch_url——照着说明书去拉数据。这个顺序是正确的,说明 AGENTS.md 里的技能调用协议生效了。
如果你看到 Agent 直接调了 fetch_url 而跳过了 read_file,说明 AGENTS.md 写得不够强硬,或者模型太「聪明」觉得自己知道怎么做。可以在 AGENTS.md 里再加一句:
markdown
**注意:即使你认为自己已经知道如何完成任务,也必须先读取 SKILL.md。这是不可违背的规定。**
听着有点蠢,但对大模型就得这么说话。

3 分钟加一个新技能
现在试试扩展能力。假设我们想让 Agent 会查汇率:
bash
mkdir backend/skills/exchange_rate
新建 backend/skills/exchange_rate/SKILL.md
markdown
---name: exchange_ratedescription: 查询两种货币之间的实时汇率。用户询问汇率、换算时使用。version: 1.0.0---# 汇率查询## Usage当用户询问货币汇率或需要换算金额时使用。## Steps1. 从用户消息中提取源货币和目标货币(如 USD、CNY、EUR、JPY)2. 使用 `fetch_url` 访问:
https://open.er-api.com/v6/latest/{源货币}
3. 从返回 JSON 的 `rates` 字段中找到目标货币的汇率4. 如果用户提供了金额,计算换算结果5. 回复当前汇率及换算金额(注明数据来源和时间)## Examples**User**:现在 1 美元能换多少人民币?**Assistant**(调用 fetch_url: https://open.er-api.com/v6/latest/USD)当前汇率:1 USD ≈ 7.24 CNY(数据来源 open.er-api.com,实时更新)
保存,重启服务。再发一条消息:「100 美元等于多少人民币?」
Agent 会自动扫到新技能,读说明书,调 API,给你换算结果。全程不改后端一行代码。
这就是技能系统最实用的地方——你只需要知道某个 API 怎么用,写一份说明书,Agent 就学会了。

让它真正「记住」你
现在 Agent 已经能查天气、查汇率了,但每次对话结束它就什么都忘了。这里要解决的就是长期记忆。
原理很直接:在 AGENTS.md 的「记忆协议」里,告诉 Agent 什么情况下把信息写入 MEMORY.md。Agent 读了这个指令,当你说「我是做 Python 后端开发的」,它就会调 python_repl 或 terminal 把这条信息追加进去。
你可以在 USER.md 里先填一部分固定信息,剩下的让 Agent 在对话中自动学习:
markdown
# backend/workspace/USER.md# 关于我姓名:[你的名字]职业:后端开发工程师技术栈:Python、FastAPI、PostgreSQL时区:UTC+8偏好:回答用中文代码风格:PEP8,尽量加类型注解不喜欢废话,直接给结论和代码
一个成熟的 MEMORY.md 大概长这样:
markdown
# 长期记忆## 关于用户- 职业:后端开发工程师,主要用 Python- 偏好:简洁代码,讨厌过度注释,PEP8 风格- 时区:UTC+8## 项目信息- 2026-05-01:正在开发 AgentClaw,一个本地 AI Agent 框架- 2026-05-09:AgentClaw 后端已跑通,下一步做前端## 用户偏好- 技术问题直接给代码,不要先解释背景- 回答语言:中文
每次启动 Agent,它都会把这个文件读进 System Prompt,「想起」你是谁、你在做什么。这才是真正的长期记忆。

几个踩过的坑
Agent stopped due to max iterations
通常是 AGENTS.md 写得太模糊,Agent 在循环调工具找不到出路。先看 verbose=True 打出来的日志,看它在循环干什么,然后在对应的 SKILL.md 或 AGENTS.md 里加更明确的指引。
root_dir 报路径错误
ReadFileTool 的 root_dir 必须用绝对路径。用 os.path.abspath() 计算,不要手写字符串。另外注意工作目录——从哪个目录启动 python app.py,相对路径的基准就是哪里。
SSE 流式输出在 Nginx 后面不流
加两个响应头:Cache-Control: no-cache 和 X-Accel-Buffering: no。代码里已经有了,如果你用了其他反向代理,查一下它对应的禁用缓冲的配置。
模型不遵循技能调用协议
换更强的模型。DeepSeek 在大多数情况下够用,但碰上复杂的多步任务有时会「偷懒」。Claude 3.5 Sonnet 对指令的遵循最严格,出现这类问题首选换它。
langchain_experimental ImportError
单独装一下:pip install langchain-experimental,这个包在主包里没有,需要显式安装。

完整依赖文件
backend/requirements.txt
fastapi>=0.111.0uvicorn[standard]>=0.30.0langchain>=0.2.0langchain-community>=0.2.0langchain-experimental>=0.0.62langchain-openai>=0.1.0langgraph>=0.1.0html2text>=2024.2.26python-frontmatter>=1.1.0python-dotenv>=1.0.0pydantic>=2.7.0

下一步能做什么
这篇文章的代码跑起来之后,你有了一个可用的本地 Agent 后端:支持流式输出、支持技能扩展、有基础的长期记忆。
但还有很多东西没做:
前端——如果你想要对话界面,用 Next.js 14 起一个项目,连接 /api/chat 的 SSE 接口就行。前端还能做 Canvas 面板(把 Agent 生成的 HTML 渲染出来)、Memory 编辑页(直接改 MEMORY.md)、技能管理页。
知识库 RAG——用 LlamaIndex 给 Agent 加一个 search_knowledge 工具,扫描本地 PDF/Markdown 文件建索引,这样 Agent 就能从你的笔记和文档里检索信息。
浏览器自动化——browser-use 库配合 Playwright,让 Agent 真正能操控浏览器,填表单、截图、爬需要登录的页面。这个后续单独写一篇。
会话持久化——现在会话历史只存在内存里,重启就没了。把它写进 sessions/{id}.json,就能保留完整对话历史。

Anthropic 官方有一个技能仓库 github.com/anthropics/skills,里面有写 Word 文档、操作表格、生成图表等现成技能,格式和这篇文章的 SKILL.md 完全兼容,可以直接拿来用。
社区还有 agentskills.io 和 github.com/VoltAgent/awesome-agent-skills,里面有各种现成技能。