# Pydantic fixed my Agent's Memory
**作者**: Akshay
**日期**: 2026-05-25T18:19:09.000Z
**来源**: [https://x.com/akshay_pachaar/status/2058976178908885210](https://x.com/akshay_pachaar/status/2058976178908885210)
---

Your agent remembers everything and understands nothing.
Agent memory started with vector databases. Store facts as chunks, retrieve by similarity.
It works until a query needs to connect facts across chunks. Then it falls apart. The problem isn't similarity. It's structure.
Knowledge graphs were the fix. Entities as nodes, relationships as edges, traversal instead of matching.
But most teams hit a different wall.
When you give an agent a knowledge graph for memory, the default behavior is that the LLM handling extraction decides the structure on its own.
It picks the entity types, the relationship labels, and the attributes.
The results are generic.
For example, you’re building a customer support agent. You feed it 50 support conversations covering customers, tickets, features, and escalation history.
You ask: “Which enterprise customers have open sev-1 tickets?”
The graph has the data. But every support ticket is stored as a “Topic” node. Every customer is an “Object.” Every relationship is “RELATES_TO.”
There’s no way to filter by type, severity, or plan tier. The query returns noise.
The agent didn’t forget anything. Nobody told it what to pay attention to.

The fix is straightforward: define the schema upfront. Tell the extraction model what types of entities exist in your domain, what relationships are valid, and what attributes each one carries.
That organizational blueprint is called an ontology. Think of it as the schema for your agent’s brain.
Let’s walk through why this matters, what breaks without it, and how to implement it it using a 100% open-source solution.
# Why flat retrieval breaks on multi-hop reasoning
Vector-based memory stores facts as text chunks and retrieves them by semantic similarity. That works until a query requires connecting facts that don’t appear in the same chunk.
Consider three facts stored about a project.
- Alice manages Project Atlas
- Project Atlas runs on PostgreSQL
- The PostgreSQL cluster went down Tuesday
A query like “was Alice’s project affected by Tuesday’s outage” needs all three.

Vector search will retrieve just facts 1 and 3 because both mention relevant terms. Fact 2 is the bridge connecting Alice to PostgreSQL through Project Atlas, but it mentions neither Alice nor Tuesday. Similarity search misses it.
A knowledge graph stores entities as nodes and relationships as edges. Instead of matching text, it traverses connections.
That chain (Alice → manages → Project Atlas → runs on → PostgreSQL) is what makes multi-hop reasoning work, and it is invisible to flat vector retrieval.
# The memory pipeline and where extraction fits
Every graph-based agent memory system follows a common pipeline:
1. Ingest: Raw data comes in (conversation messages, documents, JSON business data)
2. Extract: An LLM reads the raw data and decides what entities exist, what relationships connect them, and what attributes matter
3. Store: Extracted entities become nodes, relationships become edges, all persisted in the graph
4. Retrieve: At query time, the system searches the graph and assembles relevant facts
5. Deliver: Retrieved facts are formatted into a context block and injected into the agent’s prompt
The extraction step is where everything is decided. It determines what your graph contains, how it’s structured, and what’s queryable downstream.

Here’s the problem. In most frameworks, this step is a black box. You pass in text, an LLM pulls out “entities” and “relationships,” and you get nodes and edges. The LLM decides the types, the labels, the attributes on its own.
You have zero control over what it classifies or how.
Let's understand how to fix it.
# Defining the schema with Pydantic
The fix is the same pattern used everywhere in the AI stack.
- FastAPI endpoints get Pydantic response models.
- Function calling tools get Pydantic schemas.
- Agent memory works the same way in Zep.
Define custom entity types using EntityModel (a subclass of Pydantic’s BaseModel) with EntityText fields and descriptions that guide the extraction model.
```
from zep_cloud.external_clients.ontology import EntityModel, EntityText
from pydantic import Field
class Project(EntityModel):
"""
Represents a specific software project, application,
or codebase that the user is building or contributing to.
"""
project_status: EntityText = Field(
description="Current status: active, completed, paused, or archived.",
)
project_type: EntityText = Field(
description="Type of project: web app, mobile app, API, CLI tool, etc.",
)
```
The docstrings and field descriptions are important here because good descriptions with concrete examples give the extractor enough signal to classify accurately.
The Pydantic descriptions above aren’t just classification instructions. They teach the extractor vocabulary it doesn’t know.
A Technology entity follows the same pattern.
```
class Technology(EntityModel):
"""
Represents a programming language, framework, library,
database, or tool that the user works with.
"""
tech_category: EntityText = Field(
description="Category: programming language, framework, database, etc.",
)
```
Edge types use EdgeModel and carry their own attributes.
```
from zep_cloud.external_clients.ontology import EdgeModel
class WorksOn(EdgeModel):
"""The user is currently working on, building, or contributing to a project."""
role: EntityText = Field(
description="User's role: lead developer, contributor, maintainer, etc.",
)
class UsesTechnology(EdgeModel):
"""The user actively uses or works with a specific technology."""
proficiency: EntityText = Field(
description="Proficiency level: beginner, intermediate, advanced, or expert.",
)
```
Finally, wire these into the graph with source/target constraints using EntityEdgeSourceTarget, which defines which entity types can connect through which edge types:
```
from zep_cloud import EntityEdgeSourceTarget
client.graph.set_ontology(
entities={"Project": Project, "Technology": Technology},
edges={
"WORKS_ON": (
WorksOn,
[EntityEdgeSourceTarget(source="User", target="Project")],
),
"USES_TECHNOLOGY": (
UsesTechnology,
[EntityEdgeSourceTarget(source="User", target="Technology")],
),
},
)
```
The code enforces that
- WORKS_ON can only connect a User to a Project
- USES_TECHNOLOGY can only connect a User to a Technology.
- Any relationship that doesn't match these constraints won't produce a typed edge.
To summarise, this is what we’ve got so far:

# What happens under the hood
When a conversation is ingested with a schema active, Zep’s extraction pipeline runs five steps:
1. Entity extraction identifies named entities in the text
2. Entity resolution merges duplicates (”Nexus” and “the Nexus project” become one node)
3. Fact extraction identifies relationships and outputs them as typed edges
4. Fact resolution detects contradictions and invalidates outdated facts (preserving history)
5. Temporal extraction parses time references and maps them to validity windows on each edge

Your pydantic schema guides steps 1 and 3. Entity types tell the extractor what to look for. Edge types with their constraints tell it what relationships to classify. Resolution and temporal processing happen automatically.
# Practical walkthrough of how it looks
We ingest a conversation where a developer named Alex discusses their work (an active web app called Nexus, their tech stack, proficiency levels):

Querying for Project nodes returns Nexus with populated project_status and project_type attributes.

The node isn’t a generic “Topic” or “Object.” It’s a Project with structured fields as defined in the schema.
The edges are typed too.
- WORKS_ON carries role: lead developer

- USES_TECHNOLOGY carries proficiency: advanced for Python and Docker, proficiency: intermediate for TypeScript.

This can now filter projects by status, technologies by category, and query “which active projects use PostgreSQL” with a precise answer.
# Context templates
The final piece is context templates, which assemble typed facts into a prompt-ready block.
You can define which edge types and entity types to include, and Zep formats them with temporal annotations into a single string injected into the agent’s prompt.
```
client.context.create_context_template(
template_id="dev-context",
template="""# PROJECTS
%{edges types=[WORKS_ON] limit=5}
# TECH STACK
%{edges types=[USES_TECHNOLOGY] limit=10}
# PROJECT DETAILS
%{entities types=[Project] limit=5}
# TECHNOLOGIES
%{entities types=[Technology] limit=10}""",
)
```
It looks like this:

Every entry in the resulting context block is typed, temporally annotated, and carries the attributes defined. Save the template once, reference it by ID in agent calls.
# The 10/10/10 constraint and schema as a reasoning boundary
Zep enforces a hard limit of 10 custom entity types, 10 custom edge types, and 10 fields per type.

That’s intentional to force a dev to think about what matters in a domain rather than modeling everything.
The source/target constraints also act as guardrails on what an agent is allowed to remember. If a schema doesn’t include an edge type connecting Project to Competitor, the extraction model won’t create that relationship, even if a conversation mentions both.
The schema defines the space of valid memories.
This is the same principle behind typed function calling, where we constrain the LLM’s output space so that it can’t produce invalid arguments. Memory schemas apply that same constraint to what the agent stores.
Start with 3-4 entity types and 3-4 edge types that capture 80% of your domain logic, and add complexity incrementally.
Agent memory without schema discipline is a graph that behaves like a vector store.
In a way, you pay the cost of graph construction without getting the benefit of structured retrieval.
The schema is how you get that benefit back, and the fact that it’s Pydantic means there’s nothing new to learn.
This is especially true for domain-specific applications. LLM extraction works reasonably well on general knowledge, but the moment your domain has internal terminology, product names that collide with common words, or jargon absent from the training data, unguided extraction produces nonsense. The schema closes that gap. It carries the domain vocabulary directly into the extraction step, so the LLM doesn't need to have seen your terminology before. It just needs the definitions you wrote.

You can find Zep’s GitHub repo here → (don’t forget to star 🌟)
Thanks for reading!
## 相关链接
- [Akshay](https://x.com/akshay_pachaar)
- [@akshay_pachaar](https://x.com/akshay_pachaar)
- [11K](https://x.com/akshay_pachaar/status/2058976178908885210/analytics)
- [100% open-source solution](https://github.com/getzep/graphiti)
- [Defining the schema with Pydantic](https://github.com/getzep/graphiti)
- [You can find Zep’s GitHub repo here →](https://github.com/getzep/graphiti)
- [Upgrade to Premium](https://x.com/i/premium_sign_up)
- [2:19 AM · May 26, 2026](https://x.com/akshay_pachaar/status/2058976178908885210)
- [11.2K Views](https://x.com/akshay_pachaar/status/2058976178908885210/analytics)
- [View quotes](https://x.com/akshay_pachaar/status/2058976178908885210/quotes)
---
*导出时间: 2026/5/26 09:10:45*
---
## 中文翻译
# Pydantic 修复了我 Agent 的记忆
**作者**: Akshay
**日期**: 2026-05-25T18:19:09.000Z
**来源**: [https://x.com/akshay_pachaar/status/2058976178908885210](https://x.com/akshay_pachaar/status/2058976178908885210)
---

你的 Agent 记住了一切,却什么也没理解。
Agent 的记忆始于向量数据库。将事实作为分块存储,通过相似性检索。
这种方式一直有效,直到查询需要连接跨分块的事实。然后它就崩了。问题不在于相似性,而在于结构。
知识图谱是解决方案。实体作为节点,关系作为边,遍历而非匹配。
但大多数团队碰到了另一堵墙。
当你给 Agent 一个知识图谱作为记忆时,默认行为是负责提取的 LLM 自己决定结构。
它挑选实体类型、关系标签和属性。
结果是通用的。
例如,你正在构建一个客户支持 Agent。你喂给它 50 个涵盖客户、工单、功能和升级历史的支持对话。
你问:“哪些企业客户有未解决的 sev-1(一级严重)工单?”
图谱里有数据。但每个工单都存储为一个“Topic”(主题)节点。每个客户是一个“Object”(对象)。每种关系是“RELATES_TO”(相关于)。
无法按类型、严重程度或计划等级过滤。查询返回的是噪音。
Agent 没有遗忘任何东西。只是没人告诉它该关注什么。

修复方法很直接:预先定义模式(Schema)。告诉提取模型你的领域中存在哪些实体类型,哪些关系是有效的,以及每个实体/关系具有哪些属性。
那个组织蓝图被称为本体。把它想象成 Agent 大脑的模式。
让我们来看看为什么这很重要,没有它会发生什么,以及如何使用一个 100% 开源的解决方案来实现它。
# 为什么扁平检索在多跳推理中会失效
基于向量的记忆将事实存储为文本块,并通过语义相似性检索它们。这一直有效,直到查询需要连接不同时出现在同一个块中的事实。
考虑关于一个项目存储的三个事实。
- Alice 管理 Project Atlas
- Project Atlas 运行在 PostgreSQL 上
- PostgreSQL 集群在周二宕机了
像“Alice 的项目是否受到了周二宕机的影响”这样的查询需要全部三个事实。

向量搜索只会检索事实 1 和 3,因为两者都提到了相关术语。事实 2 是连接 Alice 和 PostgreSQL 的桥梁(通过 Project Atlas),但它既没有提到 Alice 也没有提到周二。相似性搜索漏掉了它。
知识图谱将实体存储为节点,将关系存储为边。它不是匹配文本,而是遍历连接。
那条链是多跳推理起作用的关键,而对于扁平的向量检索来说,它是不可见的。
# 记忆流水线以及提取在其中所处的位置
每个基于图的 Agent 记忆系统都遵循一个通用流水线:
1. **摄入**:原始数据进入(对话消息、文档、JSON 业务数据)
2. **提取**:LLM 读取原始数据并决定存在哪些实体,哪些关系连接它们,以及哪些属性重要
3. **存储**:提取的实体变成节点,关系变成边,全部持久化在图中
4. **检索**:在查询时,系统搜索图并组装相关事实
5. **交付**:检索到的事实被格式化为上下文块并注入到 Agent 的提示中
提取步骤是决定一切的地方。它决定了你的图包含什么,它的结构如何,以及下游可查询什么。

问题就在这里。在大多数框架中,这一步是一个黑盒。你传入文本,LLM 拉出“实体”和“关系”,然后你得到节点和边。LLM 自己决定类型、标签和属性。
你对它如何分类或如何分类没有任何控制权。
让我们了解如何修复它。
# 使用 Pydantic 定义模式
修复方案与 AI 栈中到处使用的模式相同。
- FastAPI 端点使用 Pydantic 响应模型。
- 函数调用工具使用 Pydantic 模式。
- Agent 记忆在 Zep 中也是同样的工作方式。
使用 `EntityModel`(Pydantic `BaseModel` 的子类)定义自定义实体类型,并使用带有描述的 `EntityText` 字段来引导提取模型。
```
from zep_cloud.external_clients.ontology import EntityModel, EntityText
from pydantic import Field
class Project(EntityModel):
"""
代表用户正在构建或贡献的特定软件项目、应用程序或代码库。
"""
project_status: EntityText = Field(
description="当前状态:active(活跃)、completed(已完成)、paused(暂停)或 archived(已归档)。",
)
project_type: EntityText = Field(
description="项目类型:web app、mobile app、API、CLI tool 等。",
)
```
这里的文档字符串和字段描述很重要,因为带有具体示例的良好描述能给提取器足够的信号来准确分类。
上面的 Pydantic 描述不仅仅是分类指令。它们教给提取器它不知道的词汇。
Technology(技术)实体遵循同样的模式。
```
class Technology(EntityModel):
"""
代表用户使用的编程语言、框架、库、数据库或工具。
"""
tech_category: EntityText = Field(
description="类别:programming language(编程语言)、framework(框架)、database(数据库)等。",
)
```
边类型使用 `EdgeModel` 并携带它们自己的属性。
```
from zep_cloud.external_clients.ontology import EdgeModel
class WorksOn(EdgeModel):
"""用户当前正在从事、构建或贡献的项目。"""
role: EntityText = Field(
description="用户的角色:lead developer(首席开发)、contributor(贡献者)、maintainer(维护者)等。",
)
class UsesTechnology(EdgeModel):
"""用户积极使用或使用特定技术。"""
proficiency: EntityText = Field(
description="熟练程度:beginner(初学者)、intermediate(中级)、advanced(高级)或 expert(专家)。",
)
```
最后,使用 `EntityEdgeSourceTarget` 将它们连接到图中,这定义了哪些实体类型可以通过哪些边类型连接:
```
from zep_cloud import EntityEdgeSourceTarget
client.graph.set_ontology(
entities={"Project": Project, "Technology": Technology},
edges={
"WORKS_ON": (
WorksOn,
[EntityEdgeSourceTarget(source="User", target="Project")],
),
"USES_TECHNOLOGY": (
UsesTechnology,
[EntityEdgeSourceTarget(source="User", target="Technology")],
),
},
)
```
该代码强制执行:
- `WORKS_ON` 只能将 `User` 连接到 `Project`。
- `USES_TECHNOLOGY` 只能将 `User` 连接到 `Technology`。
- 任何不符合这些约束的关系都不会产生类型化的边。
总而言之,这就是我们目前所得到的:

# 引擎盖下发生了什么
当一个对话被摄入且模式处于活动状态时,Zep 的提取流水线会运行五个步骤:
1. **实体提取**:识别文本中的命名实体。
2. **实体解析**:合并重复项(“Nexus”和“Nexus 项目”变成一个节点)。
3. **事实提取**:识别关系并将它们作为类型化的边输出。
4. **事实解析**:检测矛盾并使过时的事实失效(保留历史)。
5. **时间提取**:解析时间引用并将它们映射到每条边上的有效性窗口。

你的 Pydantic 模式引导步骤 1 和 3。实体类型告诉提取器要寻找什么。带有约束的边类型告诉它要分类哪些关系。解析和时间处理是自动发生的。
# 实际演练它的样子
我们摄入一个对话,其中一位名叫 Alex 的开发者讨论他们的工作(一个名为 Nexus 的活跃 Web 应用程序,他们的技术栈,熟练程度):

查询 Project 节点会返回 Nexus,并填充了 `project_status` 和 `project_type` 属性。

该节点不是一个通用的“Topic”或“Object”。它是一个 Project,具有模式中定义的结构化字段。
边也是类型化的。
- `WORKS_ON` 携带 `role`(角色):lead developer(首席开发)

- `USES_TECHNOLOGY` 携带 `proficiency`(熟练程度):Python 和 Docker 为 advanced(高级),TypeScript 为 intermediate(中级)。

现在可以按状态过滤项目,按类别过滤技术,并查询“哪些活跃项目使用 PostgreSQL”以获得精确的答案。
# 上下文模板
最后一部分是上下文模板,它将类型化的事实组装成准备好用于提示的块。
你可以定义要包含哪些边类型和实体类型,Zep 会将它们连同时间注释一起格式化为注入到 Agent 提示中的单个字符串。
```
client.context.create_context_template(
template_id="dev-context",
template="""# PROJECTS
%{edges types=[WORKS_ON] limit=5}
# TECH STACK
%{edges types=[USES_TECHNOLOGY] limit=10}
# PROJECT DETAILS
%{entities types=[Project] limit=5}
# TECHNOLOGIES
%{entities types=[Technology] limit=10}""",
)
```
它看起来像这样:

结果上下文块中的每个条目都是类型化的、带有时间注释的,并携带定义的属性。保存一次模板,在 Agent 调用中通过 ID 引用它。
# 10/10/10 约束和作为推理边界的模式
Zep 强制执行 10 个自定义实体类型、10 个自定义边类型和每个类型 10 个字段的硬性限制。

这是有意为之,以迫使开发者思考领域中的重要内容,而不是对所有内容进行建模。
源/目标约束也充当了关于 Agent 允许记住什么的护栏。如果模式不包含连接 Project 到 Competitor 的边类型,即使对话中提到了两者,提取模型也不会创建该关系。
模式定义了有效记忆的空间。
这与类型化函数调用背后的原理相同,我们约束 LLM 的输出空间,使其无法产生无效参数。记忆模式将同样的约束应用于 Agent 存储的内容。
从 3-4 个实体类型和 3-4 个捕获你领域 80% 逻辑的边类型开始,然后逐步增加复杂性。
没有模式约束的 Agent 记忆是一个表现得像向量存储的图。
在某种程度上,你付出了图构建的代价,却没有获得结构化检索的好处。
模式是你收回这些好处的方法,而且因为它是 Pydantic,所以不需要学习任何新东西。
对于特定领域的应用程序尤其如此。LLM 提取在通用知识上工作得相当好,但一旦你的领域具有内部术语、与常用词冲突的产品名称,或训练数据中不存在的行话,无引导的提取就会产生废话。模式弥合了这一差距。它直接将领域词汇带入提取步骤,因此 LLM 不需要以前见过你的术语。它只需要你写的定义。

你可以在这里找到 Zep 的 GitHub 仓库 →(别忘了点星 🌟)
感谢阅读!
## 相关链接
- [Akshay](https://x.com/akshay_pachaar)
- [@akshay_pachaar](https://x.com/akshay_pachaar)
- [11K](https://x.com/akshay_pachaar/status/2058976178908885210/analytics)
- [100% open-source solution](https://github.com/getzep/graphiti)
- [Defining the schema with Pydantic](https://github.com/getzep/graphiti)
- [You can find Zep’s GitHub repo here →](https://github.com/getzep/graphiti)
- [Upgrade to Premium](https://x.com/i/premium_sign_up)
- [2:19 AM · May 26, 2026](https://x.com/akshay_pachaar/status/2058976178908885210)
- [11.2K Views](https://x.com/akshay_pachaar/status/2058976178908885210/analytics)
- [View quotes](https://x.com/akshay_pachaar/status/2058976178908885210/quotes)
---
*导出时间: 2026/5/26 09:10:45*