1. 从聊天机器人到数字行动者:为什么持续运行链路总在半夜断掉
很多人第一次接触智能体,是从一个对话框开始的:问一句答一句,关掉页面就什么都不记得。这种形态我把它叫做“电子宠物”——可爱,但离开你的视线就停止工作。而 OpenClaw 这类项目展示的是另一种形态:它能接收截图、签出仓库、定位 Bug、提交代码,甚至在社交平台上回复对方。差别不在于模型更聪明,而在于它被放进了一条持续运行的链路里:有感知入口、有动态记忆、有工具调用、有失败重试、有日志可查。
问题也恰恰出在这里。你在本地把一段 Agent 脚本跑通,可能只需要十分钟;但要让它连续运行一整晚、跨多个会话记住上下文、在 API 超时后自己恢复,就会遇到一堆现实问题:上下文窗口塞满后模型开始胡言乱语、进程被系统回收、密钥散落在四五个配置文件里、报错信息只打印一行local proxy failed却不知道去哪查。这些不是模型能力问题,而是运行链路工程问题。
这篇内容聚焦一件事:用统一的 Key/API 通道把这条链路打通,让你在本地复现一条可观测、可重试、可查日志的智能体运行路径。核心检索词就是 OpenClaw 式自进化智能体、动态记忆、持续运行。适合谁看:已经会写 Python 调用大模型、想把脚本升级成常驻 Agent 的开发者;正在用 Claude Code、Cline、Codex 这类工具、被多套配置搞晕的人;以及想理解“数字行动者”到底比聊天机器人多了哪几块拼图的技术负责人。
我会按“先讲清架构跃迁 → 再统一入口 → 给可复制配置 → 跑最小验证 → 排真实报错”的顺序展开。全程不涉及任何网络加速手段,所有请求都走合规的 API 通道。你跟着做,最后能拿到一条带动态记忆读写和失败重试的最小 Agent 链路。
先说架构跃迁的三块拼图,理解了它们,后面的配置才有意义。
第一块是感知与行动的解耦。聊天机器人的输入是文本,输出是文本。数字行动者的输入可能是截图、日志、语音,输出是文件写入、命令执行、API 调用。中间需要一个规划层,把“修这个 Bug”拆成“读仓库 → 定位文件 → 改代码 → 跑测试 → 提交”。OpenClaw 用命令队列(Lane)来管理这些子任务,保证多任务不互相踩踏。
第二块是动态记忆。大多数助手用久了变笨,是因为只有当前会话的上下文。一旦超出窗口,早期信息就被截断。动态记忆的做法是把历史拆成两层:短期记忆维护当前会话,长期记忆用结构化文件(比如 JSONL 追加日志 + Markdown 摘要)持久化。需要时按任务检索相关片段,重新注入上下文。这样智能体就能记住你三周前随口提过的一个偏好。
第三块是持续运行与容错。无人值守意味着任何一次网络抖动、限流、进程崩溃都不能让整条链路死掉。需要重试策略、断点续传、以及最重要的——日志。没有日志的常驻 Agent 就是个黑盒,出了问题只能重启碰运气。
这三块拼图要落地,绕不开一个现实约束:模型调用入口必须稳定且统一。如果你的感知层用一个 Key、规划层用另一个、记忆摘要又用第三个,排查问题时根本对不上账。所以下一步先把入口收敛。
2. TaoToken 统一 Key 与 Base URL:把散落的模型入口收敛成一条通道
在动手写 Agent 之前,先解决一个容易被低估的问题:配置漂移。我见过太多项目,.env里一个 Key、settings.json里一个 Base URL、某个工具自己的配置文件里又写死一个模型名。跑单次脚本没事,一旦进入持续运行,某个环节的 Key 过期或 Base URL 写错,整条链路就在半夜静默失败。
TaoToken 在这里扮演的角色是统一入口:一个 API Key、一个 Base URL,覆盖对话、代码补全、Agent 调用等场景。官网入口是 https://taotoken.net/?utm_source=taotoken_aicg_blog_end ,API 端点是 https://taotoken.net/api 。注意 API 地址不带任何查询参数,配置时直接用它作为 base。
为什么统一入口对“持续运行”特别重要?因为常驻 Agent 的失败往往不是单点故障,而是配置不一致导致的连锁反应。统一之后,你只需要在一个地方轮换 Key、在一个地方核对模型 ID,日志里的请求也能对上同一个来源。
先把环境变量定下来。我习惯用一个.env文件集中管理,所有子进程都从这里读:
# .env —— 统一模型入口配置 TAOTOKEN_API_KEY=sk-你的实际Key TAOTOKEN_BASE_URL=https://taotoken.net/api TAOTOKEN_MODEL=claude-sonnet-4-20250514 # Agent 运行参数 AGENT_MEMORY_DIR=./agent_memory AGENT_MAX_RETRY=5 AGENT_RETRY_BACKOFF=2 AGENT_LOG_LEVEL=INFO这里有几个细节值得说清楚。TAOTOKEN_MODEL用哪个模型 ID,取决于你在控制台里开通的模型,写之前先去核对一遍,别照抄。AGENT_MAX_RETRY和AGENT_RETRY_BACKOFF是给后面的重试逻辑用的,退避倍数设为 2 表示每次重试等待时间翻倍,避免在限流时疯狂打请求。
如果你用的是 Claude Code 这类工具,它通常读settings.json。把统一入口写进去,路径和字段名要和工具要求一致:
{ "env": { "ANTHROPIC_BASE_URL": "https://taotoken.net/api", "ANTHROPIC_API_KEY": "sk-你的实际Key", "ANTHROPIC_MODEL": "claude-sonnet-4-20250514" } }注意这里的三件套必须齐全:Base URL、Key、Model ID。少任何一个,工具要么报 401,要么回退到默认端点导致请求失败。我试过只改 Base URL 忘了改 Model ID,结果工具一直报模型不存在,排查了半小时才发现是模型名对不上。
如果你用的是 Codex 系的工具,它读auth.json,结构不太一样:
{ "openai": { "base_url": "https://taotoken.net/api", "api_key": "sk-你的实际Key", "model": "claude-sonnet-4-20250514" } }字段名是base_url而不是ANTHROPIC_BASE_URL,这是不同工具的约定差异,照抄的时候要看清。Cline 或带 MCP 的编辑器,通常在设置界面里填 Base URL 和 Key,模型 ID 在下拉框里选或手填,同样三件套齐全。
统一入口之后,还有一个好处:日志归因。当你的 Agent 在凌晨三点失败,你只需要看一个端点的请求记录,而不是在五个服务的日志里翻找。这对持续运行链路的可观测性是决定性的。
配置写完,先别急着跑 Agent。用一条最简单的请求验证通道是否通,这一步能挡掉后面 80% 的玄学问题。
3. 可复制的动态记忆与持续运行配置:JSONL 追加 + 摘要回注
现在进入核心部分:把动态记忆和持续运行写成可复制的代码。这一节给的是最小可用实现,你可以直接拿去改。
先建目录结构。记忆分两层:短期用内存里的消息列表,长期用磁盘上的 JSONL 追加日志加一份 Markdown 摘要。
mkdir -p agent_memory touch agent_memory/short_term.jsonl touch agent_memory/long_term.md长期记忆的写入用 JSONL,每行一条记录,追加写不覆盖。这样即使进程崩溃,已写入的记录也不会丢。读取时按时间倒序取最近 N 条,或者按关键词过滤。
# memory.py —— 动态记忆读写最小实现 import json import os import time from pathlib import Path MEMORY_DIR = Path(os.getenv("AGENT_MEMORY_DIR", "./agent_memory")) SHORT_TERM = MEMORY_DIR / "short_term.jsonl" LONG_TERM = MEMORY_DIR / "long_term.md" def append_memory(role: str, content: str, tags: list = None): """追加一条记忆到 JSONL,带时间戳和标签""" record = { "ts": time.time(), "role": role, "content": content, "tags": tags or [] } with open(SHORT_TERM, "a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n") def recall_memory(keyword: str = None, limit: int = 10) -> list: """按关键词检索记忆,无关键词则返回最近 limit 条""" if not SHORT_TERM.exists(): return [] records = [] with open(SHORT_TERM, "r", encoding="utf-8") as f: for line in f: line = line.strip() if not line: continue try: rec = json.loads(line) except json.JSONDecodeError: continue if keyword and keyword not in rec.get("content", ""): continue records.append(rec) return records[-limit:] def summarize_to_long_term(summary: str): """把阶段性摘要写入长期记忆 Markdown""" with open(LONG_TERM, "a", encoding="utf-8") as f: f.write(f"\n## {time.strftime('%Y-%m-%d %H:%M')}\n{summary}\n")这段代码的关键设计点:JSONL 追加写保证崩溃安全;recall_memory做了 JSON 解析容错,遇到坏行跳过而不是整个读取失败;长期记忆用 Markdown 方便人直接阅读和手动修正。
接下来是持续运行的主循环,带重试和退避。这里用统一入口调用模型,把记忆检索的结果拼进上下文。
# agent_loop.py —— 带重试的持续运行主循环 import os import time import logging from openai import OpenAI from memory import append_memory, recall_memory, summarize_to_long_term logging.basicConfig( level=os.getenv("AGENT_LOG_LEVEL", "INFO"), format="%(asctime)s [%(levelname)s] %(message)s" ) log = logging.getLogger("agent") client = OpenAI( api_key=os.getenv("TAOTOKEN_API_KEY"), base_url=os.getenv("TAOTOKEN_BASE_URL") ) MODEL = os.getenv("TAOTOKEN_MODEL") MAX_RETRY = int(os.getenv("AGENT_MAX_RETRY", "5")) BACKOFF = float(os.getenv("AGENT_RETRY_BACKOFF", "2")) def build_context(task: str) -> list: """把长期记忆和短期记忆拼成消息列表""" history = recall_memory(limit=8) messages = [{"role": "system", "content": "你是一个持续运行的智能体,请基于历史记忆执行任务。"}] for rec in history: messages.append({"role": rec["role"], "content": rec["content"]}) messages.append({"role": "user", "content": task}) return messages def call_model(messages: list) -> str: """带指数退避的模型调用""" last_err = None for attempt in range(MAX_RETRY): try: resp = client.chat.completions.create( model=MODEL, messages=messages, timeout=60 ) return resp.choices[0].message.content except Exception as e: last_err = e wait = BACKOFF ** attempt log.warning(f"调用失败 attempt={attempt+1} err={e} 等待{wait}s") time.sleep(wait) raise RuntimeError(f"重试{MAX_RETRY}次仍失败: {last_err}") def run_once(task: str): log.info(f"开始任务: {task}") append_memory("user", task) messages = build_context(task) answer = call_model(messages) append_memory("assistant", answer) log.info(f"任务完成,输出长度={len(answer)}") return answer if __name__ == "__main__": result = run_once("读取 long_term.md 并总结当前记忆状态") print(result)这段代码里,build_context是动态记忆的核心:每次任务开始前,从 JSONL 里召回最近 8 条记忆,拼进消息列表。这样即使进程重启,上下文也能恢复。call_model的指数退避是持续运行的保险丝,遇到限流或网络抖动时不会直接崩掉。
依赖只需要一个:
pip install openai注意这里用的是 OpenAI 兼容的 SDK,因为 TaoToken 的 API 端点兼容这套调用约定。base_url指向 https://taotoken.net/api ,SDK 会自动拼接/chat/completions路径。如果你手动拼 URL,记得别重复加/v1。
配置和代码都齐了,下一步跑验证。
4. 验证请求与成功结果:从一次调用到连续运行的可观测链路
验证分三步走,每步都有明确的成功标志,别跳步。
第一步,验证通道连通。写一个最小脚本,只发一条消息,确认能拿到回复。
# verify_channel.py import os from openai import OpenAI client = OpenAI( api_key=os.getenv("TAOTOKEN_API_KEY"), base_url=os.getenv("TAOTOKEN_BASE_URL") ) resp = client.chat.completions.create( model=os.getenv("TAOTOKEN_MODEL"), messages=[{"role": "user", "content": "回复两个字:通了"}] ) print(resp.choices[0].message.content)运行:
export $(grep -v '^#' .env | xargs) python verify_channel.py成功标志:终端打印出模型回复的内容。如果这里就报 401,说明 Key 有问题;报连接错误,说明 Base URL 写错。这一步过了,再往下走。
第二步,验证动态记忆读写。先手动写几条记忆,再跑召回。
python -c " from memory import append_memory, recall_memory append_memory('user', '我的项目用 Python 3.11') append_memory('assistant', '已记录,后续代码按 3.11 语法生成') print(recall_memory(keyword='Python')) "成功标志:打印出包含“Python 3.11”的那条记录。如果返回空列表,检查AGENT_MEMORY_DIR路径是否正确、JSONL 文件是否有写权限。
第三步,跑完整主循环,观察日志。
python agent_loop.py成功标志:日志里依次出现“开始任务”“任务完成”,并且agent_memory/short_term.jsonl里新增了两行记录(一条 user、一条 assistant)。你可以连续跑三次,然后查看 JSONL:
wc -l agent_memory/short_term.jsonl tail -n 4 agent_memory/short_term.jsonl如果行数在增长,说明记忆在持续累积。这时候把agent_loop.py里的任务改成“回顾之前的对话,说出我提到过的 Python 版本”,模型应该能基于召回的记忆回答出 3.11。这就是动态记忆生效的直接证据。
关于持续运行,还有一个可观测性动作:把日志同时写到文件,方便半夜出问题时回看。
logging.basicConfig( level=os.getenv("AGENT_LOG_LEVEL", "INFO"), format="%(asctime)s [%(levelname)s] %(message)s", handlers=[ logging.FileHandler("agent_memory/agent.log", encoding="utf-8"), logging.StreamHandler() ] )加上 FileHandler 之后,agent_memory/agent.log会记录每次调用的时间、重试次数、错误信息。持续运行链路的排障全靠它。
到这里,你已经有一条可观测的最小链路:统一入口 → 动态记忆读写 → 带重试的调用 → 日志落盘。接下来处理真实会遇到的报错。
5. 本篇常见错排查:401、local proxy failed、reading choices、OAuth 逐个拆
这一节按真实报错来,每个都给定位方法和修复动作。
401 Unauthorized。最常见,也最好定位。原因通常是 Key 没读到、Key 过期、或者环境变量没导出。先确认环境变量真的进了进程:
python -c "import os; print(os.getenv('TAOTOKEN_API_KEY')[:8])"如果打印None,说明.env没加载。注意export $(grep -v '^#' .env | xargs)这行在 Key 含特殊字符时可能截断,稳妥做法是用python-dotenv:
from dotenv import load_dotenv load_dotenv()如果 Key 读到了还报 401,去控制台核对 Key 是否有效、是否被轮换。统一入口的好处在这里体现:只需要在一个地方换 Key。
local proxy failed。这个报错通常出现在工具层,意思是工具尝试走本地代理但失败了。注意,这里说的不是任何网络加速手段,而是某些工具自带的本地转发机制。排查方向:检查工具配置里是否残留了旧的代理地址;确认ANTHROPIC_BASE_URL或base_url指向的是 https://taotoken.net/api 而不是某个本地端口。修复动作是把配置里的代理相关字段清空,只保留 Base URL、Key、Model ID 三件套。
reading 'choices' of undefined。这是典型的响应结构不符合预期。SDK 期望resp.choices[0],但实际返回里没有choices字段。原因可能是:Base URL 写成了不带/api的地址,请求打到了错误路径;或者模型 ID 不存在,服务端返回了错误对象。定位方法:把原始响应打出来。
resp = client.chat.completions.create(...) print(resp.model_dump_json(indent=2))如果看到的是错误信息而不是正常的 choices 数组,对照错误内容修正 Base URL 或模型 ID。我踩过的坑是 Base URL 末尾多写了一个斜杠,导致路径拼接成//chat/completions,服务端返回了非预期结构。
OAuth 相关报错。某些工具(比如 Claude Code 的某些版本)默认走 OAuth 登录流程,如果你用 API Key 方式接入,需要显式关闭 OAuth 或选择 API Key 模式。排查方向:检查工具的配置文件里是否有oauth相关字段被启用;确认你填的是 API Key 而不是登录令牌。修复动作是在配置里指定使用 API Key 认证,把三件套写全。如果工具同时支持 OAuth 和 API Key,优先用 API Key,因为它在持续运行场景下更稳定,不会因为令牌过期而中断。
模型不存在 / model not found。模型 ID 写错,或者你用的模型没在控制台开通。去控制台核对可用模型列表,把TAOTOKEN_MODEL改成实际开通的 ID。注意不同工具的模型名格式可能不同,有的要带版本后缀,有的不带,照控制台显示的写。
重试次数耗尽。如果日志里连续出现调用失败 attempt=5,说明不是偶发抖动,而是持续性问题。先看错误类型:如果是限流,降低请求频率或增大退避倍数;如果是超时,检查timeout参数是否太短;如果是认证问题,回到 401 的排查路径。持续运行场景下,建议把AGENT_MAX_RETRY设到 5 以上,退避倍数设 2,给服务端足够的恢复时间。
记忆文件损坏。JSONL 追加写虽然崩溃安全,但如果某次写入被中断,可能产生半行 JSON。recall_memory里的try/except json.JSONDecodeError就是防这个的,遇到坏行跳过。如果坏行太多,可以写个清理脚本过滤掉无法解析的行。长期记忆的 Markdown 一般不会损坏,但如果手动编辑出错,直接看文件内容修正即可。
排查完这些,你的链路基本就稳了。最后说下不同场景该用哪个入口。
6. 按场景选对入口:模型对话、Coding Plan 与 API Keys 的分工
链路跑通之后,日常使用会分成几类场景,对应的入口也不一样。选对了能省不少事。
如果你只是想验证某个模型在当前任务上的表现,比如测试动态记忆召回的效果、对比不同模型的摘要质量,用模型对话入口最直接。它适合快速试错,不用改代码,把 prompt 贴进去就能看结果。验证阶段我建议先用它确认模型能力,再写进 Agent 脚本。
如果你在做长期的编码任务或 Agent 开发,需要稳定的调用配额和更完整的工程支持,Coding Plan 更合适。它面向的是持续运行场景,配额和稳定性都按长期使用设计。前面那套agent_loop.py如果要 7×24 跑,就该走这个入口。
如果你需要管理多个 Key、查看调用记录、或者给不同项目分配不同的 Key,去 API Keys 控制台。统一入口的价值在这里最大化:所有 Key 在一个地方轮换,所有调用记录在一个地方核对。排障时不用在多个服务间跳转。
接入过程中遇到配置问题,接入文档里有各工具的完整字段说明,包括 Claude Code、Cline、Codex 的配置文件路径和字段名。前面提到的三件套(Base URL、Key、Model ID)在文档里都有对照表,照着自己的工具类型查就行。
回到开头那个问题:为什么持续运行链路总在半夜断掉?答案往往不是模型不行,而是入口不统一、记忆没持久化、失败没重试、日志没落盘。这四件事做好,你的 Agent 就从“电子宠物”变成了能扛住无人值守的“数字行动者”。OpenClaw 的启示不在于它用了多强的模型,而在于它把工程细节做扎实了。你现在手里的这条链路,就是那个扎实的起点。