在本教程中,我们使用 Letta 构建一个有状态的工程 copilot,展示智能体记忆如何超越传统的基于提示词的对话。我们首先创建可检查的核心记忆块,智能体可以自主更新这些块,然后将其暴露出来,用于程序化状态同步和跨多个智能体的共享访问。我们进一步扩展该架构,加入用于语义检索的归档向量记忆、用于确定性风险估计的自定义服务端工具、用于观察智能体执行的流式传输、用于持久对话的服务端消息历史,以及用于可移植性的 .af 导出。这些组件共同展示了一个实用的记忆架构,其中持久状态、检索、工具、多智能体协作和执行轨迹作为一个集成的智能体系统协同工作。

复制代码已复制使用其他浏览器

import subprocess, sys

subprocess.run([sys.executable, "-m", "pip", "install", "-q", "-U", "letta-client"], check=True)

import os, json, time, textwrap

from getpass import getpass

from letta_client import Letta

def flex(*attempts, label=""):

err = None

for fn in attempts:

try:

return fn()

except Exception as e:

err = e

raise RuntimeError(f"All call variants failed for {label or 'call'}: {err}") from err

API_KEY = os.environ.get("LETTA_API_KEY") or getpass("Letta API key (platform.letta.com/api-keys): ")

BASE_URL = os.environ.get("LETTA_BASE_URL", "").strip()

client = flex(

lambda: Letta(base_url=BASE_URL, api_key=API_KEY) if BASE_URL else Letta(api_key=API_KEY),

lambda: Letta(base_url=BASE_URL, token=API_KEY) if BASE_URL else Letta(token=API_KEY),

label="client init",

)

def hr(title):

print("\n" + "=" * 92 + f"\n {title}\n" + "=" * 92)

def as_text(content):

if isinstance(content, str):

return content

if isinstance(content, list):

return "".join(getattr(p, "text", "") or "" for p in content)

return str(content or "")

def show(response, hide_reasoning=False):

for m in getattr(response, "messages", response) or []:

kind = getattr(m, "message_type", None) or getattr(m, "type", "?")

if kind == "reasoning_message" and not hide_reasoning:

print(" reasoning :", textwrap.shorten(getattr(m, "reasoning", "") or "", 220))

elif kind == "tool_call_message":

tc = getattr(m, "tool_call", None)

print(" tool call :", getattr(tc, "name", "?"), textwrap.shorten(str(getattr(tc, "arguments", "")), 200))

elif kind == "tool_return_message":

print(" tool ret :", textwrap.shorten(str(getattr(m, "tool_return", "")), 260))

elif kind == "assistant_message":

print(" assistant :", as_text(getattr(m, "content", "")))

elif kind == "approval_request_message":

print(" approval : 智能体正在等待你批准一次工具调用")

u = getattr(response, "usage", None)

if u:

print(f" ↳ steps={getattr(u, 'step_count', '?')} tokens={getattr(u, 'total_tokens', '?')}")

def chat(agent_id, text, max_steps=12, quiet=False):

r = flex(

lambda: client.agents.messages.create(agent_id=agent_id, input=text, max_steps=max_steps),

lambda: client.agents.messages.create(agent_id=agent_id, input=text),

lambda: client.agents.messages.create(agent_id=agent_id,

messages=[{"role": "user", "content": text}]),

label="messages.create",

)

if not quiet:

print(f"\n user : {text}")

show(r)

return r

PREFERRED = ["openai/gpt-4.1", "openai/gpt-4o-mini", "anthropic/claude-sonnet-4-20250514"]

try:

handles = [getattr(m, "handle", None) for m in client.models.list()]

MODEL = next((h for h in PREFERRED if h in handles), None) or handles[0]

except Exception:

MODEL = PREFERRED[0]

print(f" connected · model handle = {MODEL}")

hr("1. 创建一个带自定义记忆块的有状态智能体")

def make_agent(**kw):

return flex(

lambda: client.agents.create(model=MODEL, embedding="openai/text-embedding-3-small", **kw),

lambda: client.agents.create(model=MODEL, **kw),

label="agents.create",

)

copilot = make_agent(

name=f"copilot-{int(time.time())}",

description="具备可检查长期记忆的工程副驾驶",

tags=["tutorial", "copilot"],

memory_blocks=[

{"label": "persona",

"value": "我是 Mnemo,一名工程副驾驶。我说话简洁、具体,从不"

"编造事实——如果我不知道某件事,我会说出来,然后去查。",

"limit": 3000},

{"label": "human",

"description": "关于与我共事的工程师的持久事实:姓名、角色、技术栈、"

"偏好、工作风格。每当我了解到稳定信息时就更新这里。",

"value": "目前未知。",

"limit": 3000},

{"label": "project_state",

"description": "我们正在交付的项目的实时状态:里程碑、阻塞项、

决策及其理由。保持内容密集且最新;删除过时的行。",

"value": "(空)",

"limit": 5000},

{"label": "lessons",

"description": "来之不易的教训和绝不再犯的错误。仅追加,每行一条。",

"value": "(空)",

"limit": 2000},

],

}

print("agent id:", copilot.id)

def get_block(agent_id, label):

return flex(

lambda: client.agents.blocks.retrieve(agent_id=agent_id, block_label=label),

lambda: client.agents.blocks.retrieve(agent_id, label),

label="agents.blocks.retrieve",

)

def set_block(agent_id, label, value):

return flex(

lambda: client.agents.blocks.update(agent_id=agent_id, block_label=label, value=value),

lambda: client.agents.blocks.modify(agent_id=agent_id, block_label=label, value=value),

lambda: client.agents.blocks.update(agent_id, label, value=value),

label="agents.blocks.update",

)

def dump_memory(agent_id, title="core memory"):

blocks = flex(lambda: client.agents.blocks.list(agent_id=agent_id),

lambda: client.agents.blocks.list(agent_id), label="agents.blocks.list")

print(f"\n──── {title} ────")

for b in blocks:

ro = " [read-only]" if getattr(b, "read_only", False) else ""

print(f"▸ {b.label}{ro} ({len(b.value or '')}/{getattr(b, 'limit', '?')} chars)")

print(textwrap.indent(textwrap.fill(b.value or "", 100), " "))

return blocks

dump_memory(copilot.id, "core memory at birth")

我们搭建 Letta 客户端、安装 SDK,并添加兼容性辅助函数,以处理不同 Letta 版本之间的 API 差异。我们动态选择一个可用模型,并创建一个带有结构化核心记忆块的有状态工程 copilot。随后我们检查初始记忆状态,以建立该智能体的持久上下文。

复制代码已复制使用其他浏览器

hr("2. 自编辑记忆——先对话一次,再把记忆块读回来")

chat(copilot.id,

"嗨,我是 Priya,Northwind 的资深工程师。我们将在 11 月 14 日发布 'Halo',一个 Rust 摄取

服务。我讨厌冗长的解释——给我要点或代码,绝不要长篇大论。

当前阻塞项:Kafka 消费者在负载下大约每 4 分钟就发生一次再均衡。")

chat(copilot.id,

更新:我们把这次再平衡追溯到了 5s 的 max.poll.interval.ms 加上缓慢的批量提交。

决定:把提交移出 poll 循环。另外记住我们已经试过调大

session.timeout.ms,没有任何效果——不要再建议这个了。)

dump_memory(copilot.id, "两轮之后的核心记忆(记录什么移动到了哪里)")

hr("3. 你的代码写入上下文窗口")

live = get_block(copilot.id, "project_state")

set_block(copilot.id, "project_state",

(live.value or "") + "\n[CI 09:41Z] main 是红的:integration/kafka_rebalance_test 失败(已不稳定 3 次)。")

chat(copilot.id, "开始一天工作之前,有什么我应该知道的吗?")

hr("4. 两个智能体之间的共享记忆")

policies = client.blocks.create(

label="policies",

description="不可协商的工程政策。只读:遵守,绝不编辑。",

value="1. 禁止直接推送到 main。\n2. 每个修复都附带回归测试。\n"

"3. 客户数据绝不离开欧盟区域。",

limit=2000,

read_only=True,

)

scratchpad = client.blocks.create(

label="team_scratchpad",

description="copilot 与 reviewer 智能体之间的共享工作笔记。"

"两个智能体都在这里读写;条目请以你的名字作为前缀。",

value="(empty)",

limit=4000,

)

for bid in (policies.id, scratchpad.id):

flex(lambda: client.agents.blocks.attach(agent_id=copilot.id, block_id=bid),

lambda: client.agents.blocks.attach(copilot.id, bid),

label="agents.blocks.attach")

reviewer = make_agent(

name=f"reviewer-{int(time.time())}",

description="与 copilot 共享记忆的严格代码审查者",

tags=["tutorial", "reviewer"],

block_ids=[policies.id, scratchpad.id],

memory_blocks=[{"label": "persona",

"value": "我是 Argus,一个偏执的资深审查者。我寻找别人都跳过的失败模式,"

"并且按编号引用团队政策。"}],

)

print("reviewer id:", reviewer.id)

chat(copilot.id, "把你针对再平衡问题的修复方案写进 team_scratchpad "

"块,好让 reviewer 能看到。控制在 4 行以内。")

print("\n REVIEWER 在共享块中看到的内容(由另一个智能体写入):")

print(textwrap.indent(client.blocks.retrieve(scratchpad.id).value, " "))

chat(reviewer.id, "读取 team_scratchpad 并审查提出的修复方案。引用它违反的任何政策,"

然后把你的结论追加到同一个 block。

print("\n 审阅者回复后的共享 block:")

print(textwrap.indent(client.blocks.retrieve(scratchpad.id).value, " "))

print("\n 附加到此 block 的智能体:",

[a.name for a in flex(lambda: client.blocks.agents.list(block_id=scratchpad.id),

lambda: client.blocks.agents.list(scratchpad.id),

label="blocks.agents.list")])

我们通过向 copilot 提供关于工程师、项目、决策和已知故障模式的持久信息,来演示自编辑记忆。我们还直接从应用代码更新项目状态,并为多智能体协作创建共享的只读和可写 block。随后,我们将一个审阅者智能体连接到共享记忆,并观察两个智能体如何交换持久的工作上下文。

复制代码已复制使用其他浏览器

hr("5. 归档记忆:批量加载知识,然后让智能体检索它")

KNOWLEDGE = [

("事后复盘 2024-08-02:在一次部署期间消费者组重新平衡时,Halo 丢弃了 120 万条事件。

根本原因:poll 循环内的提交阻塞超过了 max.poll.interval.ms。",

["postmortem", "kafka", "halo"]),

("ADR-017:Halo 采用至少一次投递,并以 (tenant_id, event_id) 为键进行幂等写入。

出于吞吐量原因,通过 Kafka 事务实现精确一次被否决。",

["adr", "architecture", "halo"]),

("运行手册:要排空一个 Halo 节点,设置 DRAIN=1,等待 lag<1000,然后发送 SIGTERM。绝不要 SIGKILL——

在途批次只在优雅关闭时才会被检查点保存。",

["runbook", "ops", "halo"]),

("基准测试 2025-01:rdkafka 2.3 在 8 个分区下,于 c6i.4xlarge 上以 p99 32ms 持续达到 24 万事件/秒;

瓶颈是 JSON 解析,而不是网络。",

["benchmark", "performance"]),

("Priya 对文件布局的偏好:每个限界上下文一个模块,集成测试放在模块旁边,

不要 tests/ 巨型目录。", ["preference", "style"]),

("事件 2025-03-19:一个欧盟租户的负载被镜像到 us-east-1 长达 40 分钟。

通过将 DLQ 固定到区域修复。这就是策略 3 存在的原因。", ["incident", "compliance"]),

]

def archive(agent_id, content, tags):

return flex(

lambda: client.agents.passages.insert(agent_id=agent_id, content=content, tags=tags),

lambda: client.agents.passages.create(agent_id=agent_id, text=content, tags=tags),

lambda: client.agents.passages.create(agent_id=agent_id, text=content),

label="agents.passages.insert",

)

for content, tags in KNOWLEDGE:

archive(copilot.id, content, tags)

print(f"inserted {len(KNOWLEDGE)} passages")

hits = flex(

lambda: client.agents.passages.search(agent_id=copilot.id, query="losing messages during a deploy"),

lambda: client.agents.passages.list(agent_id=copilot.id, search="losing messages during a deploy"),

label="agents.passages.search",

)

print("\n programmatic semantic search for 'losing messages during a deploy':")

for h in list(hits)[:3]:

print(" •", textwrap.shorten(getattr(h, "content", None) or getattr(h, "text", ""), 150))

chat(copilot.id, "Before I restart a node tonight: is there anything in our history I should "

"worry about, and what's the safe procedure?")

hr("6. Give the agent a custom tool")

def estimate_release_risk(open_bugs: int, days_to_deadline: int, test_coverage: float) -> str:

"""Estimate the risk of shipping a release on time.

Args:

open_bugs (int): Number of open P0/P1 bugs.

days_to_deadline (int): Calendar days left before the ship date.

test_coverage (float): Line coverage as a fraction between 0.0 and 1.0.

Returns:

str: A risk score from 0-100 and a verdict.

"""

import math

pressure = open_bugs / max(days_to_deadline, 1)

score = max(0.0, min(100.0, 100 * (1 - math.exp(-1.4 * pressure)) * (1.3 - test_coverage)))

verdict = "SHIP" if score < 35 else ("SLIP ONE WEEK" if score < 70 else "DO NOT SHIP")

return f"risk_score={score:.1f} verdict={verdict}"

tool = flex(

lambda: client.tools.upsert_from_function(func=estimate_release_risk),

lambda: client.tools.create_from_function(func=estimate_release_risk),

label="tools.upsert_from_function",

)

flex(lambda: client.agents.tools.attach(agent_id=copilot.id, tool_id=tool.id),

lambda: client.agents.tools.attach(copilot.id, tool.id),

label="agents.tools.attach")

print("attached tool:", tool.name, "| agent tools:",

[t.name for t in flex(lambda: client.agents.tools.list(agent_id=copilot.id),

lambda: client.agents.tools.list(copilot.id), label="agents.tools.list")])

chat(copilot.id, "我们有 9 个未解决的 P0/P1 缺陷,距离 11 月 14 日还有 12 天,覆盖率为 0.61。"

用工具评估风险,然后把结论记录到 project_state 中。

我们用带标签的工程知识填充归档记忆,并使用语义搜索来检索相关信息,而不依赖精确的关键词匹配。然后,我们允许智能体在回答运维问题时自主查询这些归档知识。最后,我们创建并挂载一个自定义的服务端发布风险工具,使智能体能够执行确定性计算并将结果决策记录到记忆中。

复制代码已复制使用其他浏览器

hr("7. 流式输出智能体的步骤")

def stream_chat(agent_id, text):

print(f" user : {text}")

s = flex(

lambda: client.agents.messages.stream(agent_id=agent_id, input=text),

lambda: client.agents.messages.stream(agent_id=agent_id,

messages=[{"role": "user", "content": text}]),

lambda: client.agents.messages.create_stream(agent_id=agent_id,

messages=[{"role": "user", "content": text}]),

label="messages.stream",

)

for chunk in s:

kind = getattr(chunk, "message_type", None) or getattr(chunk, "type", "")

if kind == "reasoning_message":

print("", textwrap.shorten(getattr(chunk, "reasoning", "") or "", 160))

elif kind == "tool_call_message":

print("", getattr(getattr(chunk, "tool_call", None), "name", "?"))

elif kind == "assistant_message":

print("", as_text(getattr(chunk, "content", "")))

try:

stream_chat(copilot.id, "用一行给出 Halo 的状态,仅从记忆中获取。不使用工具。")

except Exception as e:

print("此 SDK 版本不支持流式输出:", e)

hr("8. 服务端对话历史")

hist = flex(lambda: client.agents.messages.list(agent_id=copilot.id, limit=8),

lambda: client.agents.messages.list(copilot.id, limit=8),

label="agents.messages.list")

for m in hist:

kind = getattr(m, "message_type", "?")

body = as_text(getattr(m, "content", "")) or getattr(m, "reasoning", "") or ""

print(f" [{kind:<24}] {textwrap.shorten(str(body), 110)}")

print("\n 仅用 id 从新进程重新连接:")

print(" client.agents.messages.create(agent_id='%s', input='...')" % copilot.id)

我们使用 Letta 的流式接口来观察智能体执行任务时的推理过程、工具调用和助手响应。随后我们检查服务端的对话历史,以验证交互状态独立于本地 Python 进程而持久存在。我们还演示了如何仅凭其持久标识符,从一个全新进程重新连接该智能体。

复制代码已复制使用其他浏览器

hr("9. 导出 / 导入智能体")

try:

dump = flex(lambda: client.agents.export_file(agent_id=copilot.id),

lambda: client.agents.export_file(copilot.id), label="agents.export_file")

blob = dump if isinstance(dump, (str, bytes)) else json.dumps(dump, default=str)

path = "/content/copilot.af" if os.path.isdir("/content") else "copilot.af"

with open(path, "wb" if isinstance(blob, bytes) else "w") as f:

f.write(blob)

print(f"exported → {path} ({len(blob)} bytes) — memory, tools and history travel with it.")

print("re-import later with: client.agents.import_file(file=open(path,'rb'))")

except Exception as e:

print("export not available on this SDK/deployment:", e)

CLEANUP = False

if CLEANUP:

for aid in (copilot.id, reviewer.id):

try: client.agents.delete(aid)

except Exception as e: print("agent delete:", e)

for bid in (policies.id, scratchpad.id):

try: client.blocks.delete(bid)

except Exception as e: print("block delete:", e)

print("deleted.")

else:

hr("Done")

print(f"copilot : {copilot.id}\nreviewer : {reviewer.id}")

print("Inspect them visually in the ADE at https://app.letta.com")

我们将完整的智能体状态导出为 .af 文件,从而提供智能体内存、工具和对话状态的可移植表示。随后我们加入了可选的清理逻辑,用于删除教程期间创建的智能体和共享块。最后我们展示持久化的智能体标识符以及 Letta ADE 的位置,以便进一步检查。

总之,我们现在已经完整地了解了 Letta 如何支持持久、可检查的智能体工作流,而不是把每次交互都当作孤立的提示词来处理。我们看到了核心记忆如何在上下文中维护高价值状态,归档记忆如何提供可扩展的检索,共享块和只读块如何协调多个智能体,以及应用代码如何直接同步外部状态。我们还使用了自定义工具和流式传输来扩展和观察智能体行为,而服务端历史和智能体导出则提供了持久性和可移植性。通过这一工作流,我们为构建工程副驾驶奠定了实用基础,使其能够持续保留上下文、基于积累的知识进行推理、跨智能体协作,并以显式、可控的记忆运行。

在此处查看完整代码。所有功劳归于该项目的研究者。另外,欢迎在 Twitter 上关注我们,别忘了加入我们拥有 15 万+ 成员的 ML SubReddit 并订阅我们的新闻通讯。等等!你在 Telegram 上吗?现在你也可以在 Telegram 上加入我们。

需要与我们合作推广你的 GitHub 仓库、Hugging Face 页面、产品发布或网络研讨会等?联系我们

文章《Stateful Agentic Engineering Copilots with Letta: Core Memory, Shared State, Semantic Retrieval, Custom Tools, Streaming, and Portable Agent Workflows》首次发布于 MarkTechPost。