Agent 系统正在从"单次问答"走向"多步自主执行",但很多团队的第一反应是把所有中间状态塞进文件——日志、JSON、Markdown、临时输出——然后让 Agent 在下一步读取这些文件继续工作。看起来简单,跑起来却很快撞墙。与此同时,另一个极端也在制造问题:把整段对话、整份文档灌进超长上下文窗口,指望模型自己"记住一切",结果信息在注意力机制里被稀释到几乎不可用。
MongoDB 的 Mikiko Bazeley 在 Real Python Podcast 第 295 期里直指这两个痛点,并提出了一个更系统的视角:上下文工程(Context Engineering)——不是给更多上下文,而是给对的上下文,用对的载体。
文件驱动的工作流,哪里会断
文件作为 Agent 间的通信媒介,有几个隐性成本:
写入时序不可控。 多个 Agent 并发写同一个目录,文件名冲突、部分写入、读到了不完整的中间结果——这些在单线程脚本里不会出现,在分布式 Agent 网络里几乎是必然的。
检索精度低。 Agent 需要的不是"整个文件",而是文件里某一行、某个字段。让 LLM 先读完整文件再提取,等于在上下文里塞了大量无关内容,既浪费 token 又干扰推理。
结构漂移。 今天 Agent A 输出的 JSON 有 status 字段,明天 Agent B 升级后改成了 state,下游 Agent 的解析逻辑就静默失效。文件没有 schema 约束,漂移是默认行为。
缺乏语义索引。 文件系统只认路径和文件名,不认内容语义。当中间产物积累到几百个文件,Agent 要找"上次关于用户权限的讨论结论",只能靠命名约定或全文扫描——两种方式都不可靠。
超长上下文窗口的注意力坍缩
GPT-4o、Claude 3.5 等模型已经支持 128K–200K token 的上下文窗口,直觉上似乎可以"把所有相关信息一次性灌进去"。实际测试反复证明:上下文越长,模型对中间段信息的召回率越低。
这不是模型笨,而是 Transformer 注意力机制的数学特性——每个 token 要对所有 token 计算注意力权重,当序列拉长,权重分布趋向均匀,真正关键的信息被淹没在噪声里。学术界称之为"Lost in the Middle"效应:模型对开头和结尾的内容召回较好,中间段显著衰减。
更糟的是,长上下文还会放大模型的"幻觉倾向"。当相关信息和不相关信息混在一个超长 prompt 里,模型更容易把不相关片段里的术语和模式拼凑成看似合理但实际错误的答案。
上下文工程:给对的,而不是给多的
Bazeley 提出的核心思路是把上下文管理当成一个工程问题,而不是一个"能塞多少塞多少"的容量问题。几个关键原则:
- 分层存储: 热数据(当前任务需要的精确片段)放 prompt,温数据(近期相关历史)放向量检索,冷数据(完整日志)放数据库,按需拉取。
- 结构化状态: Agent 间传递的状态应该是 schema-defined 的对象,而不是自由格式文本。数据库比文件更适合做这件事——字段类型、必填约束、默认值都可以在写入时校验。
- 检索增强而非全文灌入: 先用向量搜索或关键词过滤缩小候选集,再只把最相关的几段放进 prompt。这比"把整份文档贴进去"的 token 成本低,召回质量反而更高。
实践:用 MongoDB 做 Agent 状态中枢
下面是一个可运行的示例,展示如何用 MongoDB 替代文件来管理多 Agent 工作流的状态。场景:一个"研究 Agent"产出调研摘要,一个"写作 Agent"基于摘要生成文章草稿。
先安装依赖:
pip install pymongo openai
确保本地或远程有 MongoDB 实例(可用 mongod 启动本地实例,或用 MongoDB Atlas 免费集群)。
import pymongo
import openai
import json
from datetime import datetime
# ── 1. 连接 MongoDB,创建带 schema 校验的集合 ──
client = pymongo.MongoClient("mongodb://localhost:27017/")
db = client["agent_workflow"]
# 用 MongoDB 的 JSON Schema 校验确保状态结构不漂移
db.create_collection("task_state", validator={
"$jsonSchema": {
"bsonType": "object",
"required": ["task_id", "agent", "status", "output", "created_at"],
"properties": {
"task_id": {"bsonType": "string"},
"agent": {"bsonType": "string"},
"status": {"enum": ["pending", "done", "failed"]},
"output": {"bsonType": "string"},
"created_at": {"bsonType": "date"},
"metadata": {"bsonType": "object"} # 可选的附加字段
}
}
})
col = db["task_state"]
# ── 2. 研究 Agent:产出调研摘要并写入数据库 ──
openai.api_key = "sk-..." # 替换为你的 key
def research_agent(topic: str) -> str:
"""调用 LLM 做调研,返回摘要文本"""
resp = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "你是一名技术调研员,输出简洁的要点摘要。"},
{"role": "user", "content": f"调研主题:{topic}。列出 3-5 个核心要点。"}
],
temperature=0.3
)
return resp.choices[0].message.content
task_id = "ctx-eng-001"
summary = research_agent("上下文工程在 Agent 系统中的实践")
# 写入状态——schema 校验会拦截缺失字段或类型错误
col.insert_one({
"task_id": task_id,
"agent": "research",
"status": "done",
"output": summary,
"created_at": datetime.utcnow(),
"metadata": {"topic": "上下文工程", "tokens_used": "~200"}
})
print("研究 Agent 输出已写入 MongoDB")
print(summary[:120], "...")
# ── 3. 写作 Agent:从数据库精确读取上游状态 ──
def writing_agent(task_id: str) -> str:
"""只拉取指定 task 的 output,不读全表"""
state = col.find_one(
{"task_id": task_id, "agent": "research", "status": "done"},
{"output": 1} # 只取 output 字段,不取 metadata 等无关内容
)
if not state:
raise ValueError(f"未找到 task {task_id} 的研究产出")
research_output = state["output"]
# 只把相关摘要放进 prompt,而不是整段对话历史
resp = openai.chat.completions.create(
model="gpt-4o-mini",
messages=[
{"role": "system", "content": "你是一名技术作者,基于调研摘要写一段通俗解读。"},
{"role": "user", "content": f"调研摘要如下:\n{research_output}\n\n请写一段 200 字以内的通俗解读。"}
],
temperature=0.5
)
return resp.choices[0].message.content
draft = writing_agent(task_id)
# 写入写作 Agent 的状态
col.insert_one({
"task_id": task_id,
"agent": "writing",
"status": "done",
"output": draft,
"created_at": datetime.utcnow(),
"metadata": {"model": "gpt-4o-mini"}
})
print("\n写作 Agent 草稿已写入 MongoDB")
print(draft)
这个示例的关键设计点:
- Schema 校验阻止了字段名漂移——如果某个 Agent 把
status写成state,MongoDB 会直接拒绝写入。 find_one只取output字段,避免把metadata等无关信息塞进下游 prompt,这就是"给对的上下文"的具体实现。- 状态可追溯——每个 Agent 的产出都有
task_id、agent、created_at,调试时可以精确回溯任意步骤。
向量检索层:当候选集很大时
如果 Agent 需要从几百条历史状态里找"和当前任务最相关的几条",全文灌入不现实,关键词匹配太粗糙。这时候在 MongoDB 上加一层向量索引:
# MongoDB Atlas Vector Search 示例(需 Atlas 集群)
# 先为集合添加向量字段
from openai import OpenAI
oai = OpenAI()
def embed_text(text: str) -> list[float]:
resp = oai.embeddings.create(model="text-embedding-3-small", input=text)
return resp.data[0].embedding
# 写入时同步生成向量
doc = col.find_one({"task_id": task_id, "agent": "research"})
col.update_one(
{"_id": doc["_id"]},
{"$set": {"embedding": embed_text(doc["output"])}
})
# 查询时:用当前任务的描述向量做相似搜索,只取 top-3
query_embedding = embed_text("如何减少 Agent 上下文中的噪声")
pipeline = [
{
"$vectorSearch": {
"index": "vector_index", # 需在 Atlas 中预先创建
"path": "embedding",
"queryVector": query_embedding,
"numCandidates": 50,
"limit": 3
}
},
{"$project": {"output": 1, "metadata": 1, "score": {"$meta": "vectorSearchScore"}}}
]
results = list(db["task_state"].aggregate(pipeline))
for r in results:
print(f"[score={r['score']:.3f}] {r['output'][:80]}...")
这样 Agent 拿到的不是"所有历史文件",而是"和当前意图最相似的三段产出"——上下文窗口里只有高信号内容,注意力坍缩的风险大幅降低。
采纳建议与取舍清单
| 决策点 | 推荐做法 | 代价 |
|---|---|---|
| Agent 间状态传递 | 用数据库 + schema 校验,不用文件 | 需要数据库依赖,本地调试多一层基础设施 |
| 长文档/长历史 | 向量检索 top-k + 精确字段投影,不全文灌入 | 需维护向量索引,检索有延迟 |
| 临时中间产物 | 可用文件,但限定为单 Agent 内部、不跨 Agent 传递 | 文件仍适合做纯本地缓存 |
| 上下文窗口预算 | 烙定硬上限(如 4K token 给背景,2K 给指令,留 2K 给输出) | 需要人工测量和调整,不是一劳永逸 |
| Schema 漂移防护 | 数据库端校验 + 代码端版本号 | 升级时要做迁移,但比静默失效可控 |
一句话总结:文件是本地缓存的好工具,却是跨 Agent 通信的坏协议。超长上下文是应急输血,不是长期营养。 把状态管理从"能跑就行"升级到"有 schema、有检索、有分层",是 Agent 系统从 demo 走向生产的关键一步。