给 AI 助手装上长期记忆:OpenClaw + Amazon Bedrock AgentCore 实战

AWS ML Blog 发布日期待确认

现成的 AI 助手能答好单个问题,却记不住你是谁。今天你问它花园的事,它不知道你三周前提过排水快的高架花床,也不知道你只用有机肥。本文用开源智能体系统 OpenClaw,跑在 Amazon Bedrock AgentCore 的运行时上,配合 AgentCore 记忆和结构化元数据,搭出一个能持续积累上下文的个人助手,整套系统一条命令即可部署。

问题不在回答质量,而在"记不住"

现成的 AI 助手在回答单个问题上表现不错,但在另一个维度上明显吃力:连续性。

你问一个无状态(stateless,即每次请求都不带历史、彼此独立)的助手关于花园的问题,它完全不知道你三周前提到过排水很快的高架花床,不知道你只用有机肥,也不知道你的矮牵牛正熬过一波热浪。每次对话都从零开始,用户只能一遍遍重新解释背景。真正的短板不是回答质量,而是助手对你没有记忆。

这篇文章会展示如何构建一个不断积累上下文的个人助手:用开源智能体系统 OpenClaw,运行在 Amazon Bedrock AgentCore 的 AgentCore 运行时上。其中 AgentCore 记忆能把用完即弃的聊天变成长期可用的知识。你还会看到如何给这些记忆打上结构化元数据标签,方便取出与当前问题相关的记录。

贯穿全文的例子是园艺助手 Sprout,但这套架构并不绑定领域。换一套角色设定和技能清单,同一条流水线也能服务于客服机器人、健身教练或企业内部帮助台。整个系统放在一个 AWS CloudFormation 模板里,一条命令即可部署,按用量计费,轻量个人使用每月只要几美元。文中还会分享一些设计准则,供你构建同类助手时参考。

解决方案概览

AgentCore 是一个用来大规模构建、连接和优化智能体的平台,支持任意框架或模型。下图展示了端到端的请求流:从 Telegram 传入的 webhook(即平台主动推送消息给你的服务的回调),经过 AgentCore 运行时,以及它依赖的各项 AWS 服务。

图 1:Telegram webhook 和 Amazon EventBridge 调度都会调用同一个 AgentCore 运行时智能体,由它协调 OpenClaw 网关、AgentCore 记忆和 Amazon Bedrock。

两个入口汇聚到同一个智能体:

两者都会调用 AgentCore 运行时上的 InvokeAgentRuntime API。在这里,一个轻量的 server.py 进程负责协调 OpenClaw 网关、AgentCore 记忆和 Amazon Bedrock Converse API。

周边配套也一并说明:工作区存储由 Amazon S3 提供,加密交给 AWS KMS,bot token 存放在 AWS Secrets Manager,日志和指标则由 Amazon CloudWatch 收集。

部署前要准备什么

如果想用 Launch Stack 按钮或 scripts/deploy.sh 脚本(详见后文"自主扩展"一节)部署自己的版本,需要准备:

架构:AgentCore 运行时上的无服务器 agent

所有组件都放在同一个 CloudFormation 模板里,启动时不需要任何构建工具。下面几节逐一说明关键的设计决策。

只为活跃算力付费

agent 运行在 AgentCore 运行时的一个容器里,采用按用量计费模式。你只为 agent 实际消耗的算力付费,而不是按常驻在线时长计费;等待模型响应这类 I/O 操作的时间也不收费。

对个人助手这种短时使用的场景,算下来月费基线大约 1–2 美元;相比之下,一台全天候运行的 Amazon EC2 实例每月约 35 美元。这些数字是截至 2026 年 7 月针对轻度个人使用的估算,最新价格请参考 AgentCore 定价页面。

容器要遵守的最小合约

运行时要求容器满足一套最小约定:监听 8080 端口,提供 GET /ping 作为健康检查,以及 POST /invocations 作为 agent 的入口。

我们的容器是 linux/arm64 架构,采用多阶段构建:先基于 OpenClaw 官方镜像,再叠加一层 Python。

关键要点

用 OpenClaw 作为智能体底座

OpenClaw 负责智能体的主循环、工具调用和技能系统。为了让它符合 AgentCore 的 HTTP 协议约定,我们在外面又包了一层脚本 server.py:

有一点必须强调:AgentCore 有可能恢复一个已经冻结的容器,而容器里的子进程其实早就退出了。所以调用路径不能想当然地认为 gateway 还活着,而是先调用 ensure_openclaw_ready() 重新检查健康状况(必要时重启 gateway),再把这一轮对话转发过去。

这种包装思路可以推广:任何以本地进程形式运行的智能体框架,都能用同样的方式接进 AgentCore 运行时,而且不用改动框架本身。

两个模型,按任务各管一块

文本聊天和图像理解在成本、质量上的取舍完全不同,所以助手把它们分别路由到 Bedrock 上的两个 Claude 模型:

文本回合走 OpenClaw gateway,这样能用上技能和会话状态;图像回合则由 server.py 直接调用 Bedrock 的大语言模型,把图片字节作为多模态内容块传进去。

我们故意让图像绕开 gateway:容器内那版 OpenClaw 会在内容到达 Bedrock 之前把 image_url 部分丢掉,所以必须从 server.py 直接调 Converse API,才能保证模型看到真正的像素。

两条路径共用同一套系统提示词(人设 + 记忆),体验保持一致。模型 ID 放在环境变量里(MODEL_ID、VISION_MODEL_ID),这样每次换模型部署都不必重新构建镜像。

技能:可复用的能力单元

在社区技能清单 community-skills.json 里,能力以「技能」的形式声明。部署脚本会在构建镜像之前把这些技能写进容器,并登记到 OpenClaw 的配置中。截稿时,Sprout 自带天气、提醒和植物笔记三项技能。只要换一份清单,同一套流水线就能服务另一个领域——正是这一点,让它成为一个可复用的模式,而不只是某个具体的机器人。

Telegram:无服务器的前门

做个人助理,Telegram 是很实用的入口:它基于 Webhook(网络回调,服务器主动把消息推给你的接口),整套流程都能保持无服务器。它不需要你开发客户端,用户手上任何设备都能用,通过一套简单的机器人 API 就能处理文本、图片和富文本格式。BotFather 会签发一个机器人令牌(bot token),存进 Secrets Manager。部署时注册一个 Webhook,让 Telegram 把消息指向 API 网关的端点。用户发出消息后,Telegram 会把它投递给 Webhook 的 Lambda 函数,由函数校验载荷,再调用 InvokeAgentRuntime。回复则沿着 Telegram 机器人 API 原路返回。

关于格式,有一条经验值得记住:Telegram 那套老式 Markdown 对未转义字符非常苛刻,模型回复里只要多出一个下划线,整条消息就可能发不出去。改用 HTML 渲染回复更可靠,所以助理在发送前,会把模型输出转换成 Telegram 能安全解析的 HTML。

记忆:把一次性聊天变成长期知识

上面这套架构已经是一个能力不错、成本低廉的无服务器代理,但它自己不会记住你——聊完就忘。记忆才能改变这一点。设想你几周前提过自己是有机种植,今天助理推荐了一套处理方案,还主动补了一句:它选了有机的方案,因为你不用合成肥料。无状态的模型做不到这一点。

思路是:短期存事件,长期做提炼。

AgentCore 的记忆分两层。短期记忆通过 CreateEvent,把每一轮对话都存成一个事件,用 actorId(也就是 Telegram 的聊天 ID)和 sessionId 做键。这是原始的对话记录。

长期记忆由托管抽取策略异步生成,落成持久化的结构化记录。我们配置了三种策略:

命名空间:一位园丁一个花园

命名空间隔离:每个用户一个独立抽屉

Sprout 会把记忆写入每位用户各自的命名空间,两个聊天记录永远不会混在一起:

chat_id 是唯一会变化的片段。这样一来,隔离逻辑既好推理也好测试:每位园丁唯一对应一个命名空间,不会和别人撞车。

检索、组装、注入:一轮对话的记忆流水线

每一轮对话,Agent 都会先检索相关的长期记录,排序后再注入系统提示词。下面是一条消息进来时,server.py 里发生的主要动作。

第一步:检索

用用户的消息作为查询词,对 sprout/{chat_id}/long_term 调用 RetrieveMemoryRecords,最多返回 50 条,限时 3 秒。如果检索超时或出错,我们选择优雅降级——不带记忆直接回答,而不是让整个流程挂掉。

try:
    records = memory_client.retrieve_memory_records(
        memoryId=MEMORY_ID,
        namespace=f'sprout/{chat_id}/long_term',
        searchCriteria={
            'searchQuery': user_message,
            'topK': 50,
            'metadataFilters': []
        },
    )  # 3s timeout
except Exception:
    records = []  # fall back to answering without memory

片段 1:为当前这一轮对话检索长期记忆记录(示意代码,完整源码见仓库)。

第二步:组装

组装函数还加了一些自定义逻辑。我们希望显式偏好排在推断事实之前,同一类别内保持稳定顺序,并在注入前对结果做数量上限。

def assemble(records, cap=50):
    explicit = [r for r in records if r.type == 'USER_PREFERENCE']
    inferred = [r for r in records if r.type != 'USER_PREFERENCE']
    # explicit beats inferred; stable order within each class
    ordered = explicit + inferred
    return ordered[:cap]

片段 2:组装这一步把显式偏好排在推断事实之前。

元数据:在命名空间内部再给记忆分组

命名空间回答的是「这条记忆属于谁」,元数据回答的是「它讲的是什么」。在 sprout/{chat_id}/long_term 里搜一句「我的矮牵牛蔫了」,所有语义上相近的内容都会被返回。对一位园丁来说,这意味着连同三月份的一条施肥偏好、一条无花果修剪记录,都会和真正相关的记录挤在一起。而结构化元数据能帮我们在内容送进提示词之前,先把记忆范围缩小。

这里有一条规则决定了所有取舍:一个元数据键只有在被声明为索引键之后,才能在服务端做过滤。 更多细节可以看《Structured memory filtering with metadata in Amazon Bedrock AgentCore Memory》。在这个例子里,Sprout 用了三个索引键:

IndexedKeys:  # on the AWS::BedrockAgentCore::Memory resource
  - Key: type  # seperate the kinds of records
    Type: STRING
  - Key: section  # which bed or area it describes
    Type: STRING
  - Key: plants  # what is growing there
    Type: STRINGLIST

每个条目指定一个键名(必须与已建立索引的键匹配才能过滤),并把 extractionType 设为 STRICTLY_CONSISTENT(直接从事件透传)或 LLM_INFERRED(从对话中提取)。对于推断键,提取配置可以把取值限制在一个固定列表里。Sprout 正是这么做的,因此两条写入路径会生成相同的词汇表——无论记录由哪部分创建,过滤的含义都一致。

持久化对话轮次,形成闭环

模型响应后,server.py 调用 CreateEvent,同时写入用户轮次和助手轮次。新事件会喂给提取策略,为下次使用丰富长期存储。

查看原文