User as Code:面向个性化 Agent 的可执行记忆

  • 关联论文:2606.16707
  • 作者:flyP
  • 更新:2026-09-12

一句话结论

把 Agent 对用户的建模从「bag-of-facts」检索式记忆,改为「活的 Python 软件项目」——类型化对象承载状态、普通函数编码规则、append-only 日志定期 checkpoint 进代码——在标准 long-term conversation 基准(LOCOMO)召回上追平 SOTA(78.8%),并在「聚合型问题」上把检索式记忆的 6–43% 直接拉到 99%,且原生支持确定性的安全告警(如药物过敏冲突)。

解决什么真问题

个性化 Agent(personalized AI agent)需要一个跨多轮对话持久维护的「用户模型」。主流做法是 unstructured text / knowledge graph / flat store of facts,并由 retrieval 在每次新请求时召回相似条目。这条路线有四个结构性缺陷:

  • 真问题 1:bag-of-facts 召回好但「思考」弱。可以召回单条事实,但存储和行动分离,无法解决矛盾、跨多条记录做聚合、或者强制规则。
  • 真问题 2:聚合问题崩塌。「去年我出过几次国际差旅?」这类问题本质是对结构化状态的一次计算;检索式记忆把它当作相似度搜索,正确率只剩 6–43%。
  • 真问题 3:矛盾无法收敛。同一事实的新陈述与旧陈述冲突时,纯文本/纯检索路径只能靠 LLM 自圆其说,没有 ground truth。
  • 真问题 4:安全敏感规则不可执行。「新开的药方与三个月前记下的过敏冲突」这类规则,需要「状态一旦变化,规则就立刻重算」——query-driven 的检索式记忆天然做不到。

核心方法

1. 范式:User as Code(UaC)

UaC 把「用户模型」视为一个活的软件项目:

  • 状态层:用类型化 Python 对象(typed Python objects)表达用户状态——比如 allergies: Set[str]、trips: List[Trip]、prescriptions: List[Drug]。
  • 规则层:用普通 Python 函数(ordinary Python functions)编码治理规则——比如 def drug_conflict_check(new_drug)、def aggregate_trips(year)。
  • 运行时层:表示与推理发生在「解释器可运行的同一媒介」——同一个 Python 进程里。

存储与行动从此在同一介质中:状态变化 → 规则重算 → 告警/聚合结果立刻可得。

2. 使能机制:两阶段 pipeline

UaC 的关键设计是一个两阶段 ingest pipeline,让 memory 既可执行、又不会因为「重写代码」丢失原始事实。

  • 阶段 A:append-only log。所有用户陈述(对话、偏好、状态变化)按到达顺序写入只追加日志,从不删除事实。这是 UaC 的「事实无损」保证。
  • 阶段 B:周期 checkpoint into typed code。定期把日志 fold 成类型化代码与对象——合并重复、解析矛盾、抽出规则。这一步把「自然语言事实流」转化为「结构化可执行状态」。
# UaC 简化骨架
class User:
    allergies: Set[str]
    trips: List[Trip]
    prescriptions: List[Drug]

def drug_conflict_check(new: Drug) -> List[str]:
    return [a for a in user.allergies if conflicts(new, a)]

# append-only log + 周期 checkpoint
def ingest(event):
    log.append(event)              # 事实无损
    if should_checkpoint():
        code = fold(log)           # 把 log 折成 User + 函数
        exec(code, user_ns)        # 重新加载到解释器

这条 pipeline 同时回答了「不能丢事实」与「代码要可执行」两个原本看似冲突的需求。

3. 聚合型问题的「一行计算」优势

聚合问题的关键洞察:答案不是检索结果,而是对 typed state 的一次确定计算。

  • 例:「去年我出过几次国际差旅?」→ len([t for t in user.trips if t.year == last_year and t.is_international])
  • 检索式记忆要把这个问题转成「last_year international trip」相似度查询,召回相关文本段落,再让 LLM 数出来——召回错了就崩塌。
  • UaC 直接对结构化字段 filter + 计数,因为 trips 列表里每条都是 typed object。

这是 abstract 中给出的最大反差:retrieval 6–43% vs UaC ~99%。

4. 安全告警:状态变化即触发

UaC 的规则在状态变化时同步执行。例如「新药方与已知过敏冲突」:

  • 旧系统:用户在几个月后的某次对话里问「我最近吃什么药?」时,LLM 不会主动去对照新处方和旧过敏——因为它是 query-driven,不是 event-driven。
  • UaC:每条 prescriptions.append(new) 触发 drug_conflict_check(new),冲突即抛 unsolicited alert。

这条能力是 query-driven memory 「无法提供」的能力。

关键实验与数据

  • LOCOMO(long-term conversation 基准)召回:UaC 达到 78.8%,与 full-context upper bound 和最强 prior memory systems 持平。
  • 聚合型问题:retrieval-based memory 正确率 6–43%,UaC 接近 99%。
  • 安全敏感告警:UaC 原生支持 unsolicited safety-critical alerts(药物-过敏冲突)——这是 query-driven 路线结构上做不到的。
  • 定性机制:append-only log + periodic checkpoint = 事实无损 + 代码可执行。

⚠️ LOCOMO 基准出自 Maharana et al., ACL 2024(Snap Research),原文中已注明。⚠️ abstract 没有给出 LOCOMO 的细分子分数(单跳/多跳、跨会话等)、聚合问题的具体子集规模、安全告警的 false positive rate 等细节。原文未明确。

亮点

  1. 范式级抽象:把「user memory」从「文本/向量」挪到「软件项目」。这是抽象层级(representation level)的提升,而不是某个模块的微调。
  2. append-only log + checkpoint 兼顾两难:既不丢事实,又让代码可执行——常见 memory 系统的「重写即丢」陷阱被绕开。
  3. 聚合问题近满分:6–43% → 99% 的反差是 abstract 中最具说服力的单条证据。
  4. 原生安全告警:把「event-driven 规则触发」从「LLM 自觉」变成「解释器语义」,安全敏感场景下是质变。
  5. LOCOMO 追平 SOTA:在「普通召回」维度不输,证明 UaC 不是靠牺牲常规指标换聚合优势。
  6. 工程可落地:Python 对象 + 普通函数 = 任何团队都能在两周内搭出 MVP。

局限与待核

  1. ⚠️ checkpoint 的「何时折、如何折」是核心工程问题——abstract 没给具体调度策略与合并规则。
  2. ⚠️ 矛盾解析(同一事实的新旧陈述冲突)的具体冲突解决算法在 abstract 中未明确。
  3. ⚠️ 当 User 对象规模膨胀时,规则链的复杂度管理未在 abstract 中讨论(执行顺序、循环依赖等)。
  4. ⚠️ 跨会话、跨设备的 checkpoint 同步策略与一致性保证 abstract 未明确。
  5. ⚠️ 「LLM 写的代码本身就是新攻击面」——若 checkpoint 阶段由 LLM 生成 typed code,安全/正确性验证如何做?原文未明确。
  6. ⚠️ LOCOMO 之外的 long-term benchmark(e.g. MSC、LoCoMo 扩展、UltraChat 记忆子集)是否同表现待核。
  7. ⚠️ aggregate 99% 是单数据集结果,跨域泛化待核。
  8. ⚠️ 原文 abstract 未给出 GitHub 仓库链接,UaC 目前无法直接获取实现代码。

对工程落地的启发

  • 谁先用得上:做个性化 Agent、AI 助理、个人知识库、agent-as-coworker 的团队。
  • MVP 接入路径:用 Python dataclass / pydantic 定义 User 类,先把最常用的 5–10 条规则写成函数(如冲突检查、聚合统计);append-only log 用 SQLite/JSONL;checkpoint 频率先按每日/每周手动触发,再自动化。
  • 与现有 memory 系统共存:UaC 不必替代 vector store,可以让 vector store 负责「自由文本回忆」,UaC 负责「结构化状态 + 规则」。两条路各管一摊。
  • 安全敏感场景的最小闭环:药物过敏、金融风险偏好、儿童保护规则——这些用 UaC 实现 1 个 alert function 通常 < 100 行代码,但能挡掉真实事故。
  • 类比 OpenClaw:如果你做的 agent 需要在多轮/多会话后保持一致的用户模型,且对「不能丢事实」「必须主动告警」有要求,UaC 是直接对路的抽象层级——比纯向量召回更可工程化。

与同方向工作的关系

  • vs Retrieval-based memory(MemGPT、MemoryBank、A-Mem 等):这是 UaC 主要对比对象。LOCOMO 上 UaC 追平 SOTA,但在聚合 + 主动告警两条上结构性胜出。
  • vs Knowledge Graph memory:KG 提供结构但难聚合「跨多条记录的统计」;UaC 把 KG 的结构优势与代码的执行能力合并。
  • vs Toolformer / ReAct 风格的 agent:这些是「行动」抽象,UaC 是「状态 + 规则」抽象——两者正交,可叠加。
  • vs Program-aided LM(PAL / Program-of-Thought):PAL/PoT 是「用代码辅助单次推理」,UaC 是「用代码持久表示用户」——使用代码的语境和生命周期都不同。
  • vs Profiles / System Prompt 注入:传统做法是把用户画像塞进 prompt,受上下文窗口与「模型是否真的会读」双重制约;UaC 把画像从 prompt 里拿出来,放进运行时。

适合谁读

  • 个性化 AI Agent / 个人助理方向的工程师与 PM
  • Agent 长期记忆 / Memory architecture 方向的研究者
  • 安全敏感场景(医疗、金融、儿童)产品负责人
  • 对「representation matters」方法学感兴趣的 LLM 应用研究者
  • ⚠️ 不适合:只做一次性对话、无长期记忆需求的 chatbot 团队

§0 元层五问(写作自检)

  • R1 命名反方:是否仅是「又一个 memory 系统」?答:不是。它把抽象层级从「文本/向量」提升到「软件项目」,并把「存储 = 行动」合并。
  • R2 工程反方:checkpoint 调度与矛盾合并的具体算法?原文未明确。
  • R3 安全反方:LLM 生成的 typed code 本身是否引入新攻击面?原文未明确。
  • R4 数据反方:聚合 99% 是否跨数据集泛化?原文未明确。
  • R5 边界反方:UaC 适用边界(用户状态稳定 vs 剧烈变化)?原文未明确。
  • 撞名检查:本标题与本目录下其他 explainer 无重复。
  • 边界:仅写本文件 promo/explainers/2606-16707.md。

工程落地与核查(Jay)

事实核查摘要

声明 核查结果
LOCOMO 召回 78.8% 追平 SOTA ✅ 原文 abstract:"78.8% on LOCOMO";LOCOMO = Maharana et al., ACL 2024
聚合问题 retrieval 6–43% vs UaC 99% ✅ abstract:"retrieval-based memory collapses (6-43%) while UaC stays near-perfect (99%)"
药物-过敏冲突 unsolicited alert 示例 ✅ abstract 原文示例
checkpoint 两阶段 pipeline ✅ abstract 原文
append-only log 事实无损 ✅ abstract 原文
⚠️ 存疑:各 LOCOMO 子类分数 abstract 未给出,读 PDF §X
⚠️ 存疑:checkpoint 调度算法 abstract 未给出
⚠️ 存疑:矛盾合并规则 abstract 未给出
⚠️ 存疑:安全/攻击面验证方案 abstract 未给出
⚠️ GitHub URL abstract 未给出,当前无公开代码仓库

可读性精修

  • 全文术语统一性良好,「UaC」作为缩写首次出现有 full name。
  • §4「状态变化即触发」段落中「新药方与已知过敏冲突」例子与 abstract 原文一致,建议保留为具体锚点。
  • §1「bag-of-facts」一词在首次出现时应加引号注明其含义,后文使用保持一致。
  • §3「typed object」和「type化 Python 对象」混用,建议统一为「类型化 Python 对象」或「typed Python object」。

工程落地:实际系统怎么用,坑在哪

MVP 搭建路径(2–4 周可出 demo):

Week 1: 状态建模
  - 用 pydantic 定义 User 类(UserProfile, Allergy, Trip, Prescription 等)
  - 确定 append-only log 格式(推荐 JSONL,每行一个事件)
  - 写最基础的 ingest(event) 和 checkpoint()

Week 2: 规则编码
  - 上线 3–5 条核心规则(冲突检测、聚合统计)
  - 用真实对话数据跑一轮,验证规则触发逻辑

Week 3–4: 与现有 Agent 对接
  - UaC 作为独立服务,Agent 通过 API 调用
  - LLM 只负责把用户陈述转为结构化事件,规则执行由 Python 解释器负责

必踩的 5 个坑:

  1. checkpoint 时机选择:若频率太高(每句话都 checkpoint),则失去「合并矛盾」的机会;若太低(每月一次),则内存中 User 对象与 log 的偏差累积。推荐:按对话 session 边界触发 + 每日增量合并。
  2. 矛盾解析没有银弹:同一用户两次说「我没有过敏」——是纠正了旧记录、还是信息不确定?UaC 的 append-only log 保证了事实不丢失,但「哪条是对的时候」需要业务规则或人工审核介入,不能推给 LLM 自圆其说。
  3. typed code 生成的安全性:若 checkpoint 阶段用 LLM 生成 Python 代码,直接 exec() 是 RCE 漏洞。必须沙箱化(subprocess / Docker container / RestrictedPython),或在生成后加类型注解审查。
  4. 冷启动问题:UaC 对新用户(log 为空时)没有记忆,需要 fallback 到传统 retrieval memory 或 system prompt——两种记忆路径的切换逻辑需要显式设计。
  5. 序列化与迁移:User 对象 checkpoint 成 .py 文件后,若格式变化(如加字段),历史 checkpoint 如何 migration?建议用 JSON 或 msgpack 序列化,避免直接 pickle Python 对象。

与 vector store 的混合架构(推荐生产使用):

用户输入 → LLM 解析事件 → UaC 状态更新
                ↓
          检索记忆(vector store)→ 回答"我上个月去过哪些地方"(简单回忆)
          UaC 规则引擎     → 回答"我去年国际出差几次"(聚合计算)
                           → 主动推送"你的新药和已知过敏冲突"

两套系统各司其职:检索记忆负责「回忆」,UaC 负责「计算 + 告警」。

适用规模估算: - 单用户 < 10,000 条事件:Python in-memory 对象 + JSONL log 完全够用 - 单用户 10,000–100,000 条:需要定期压缩/合并 log,或迁移到 SQLite - 100,000+ 条/用户:建议对象数据库(如 objectsDB)或时序数据库,专门做 checkpoint 版本管理