Almanac解决多Agent系统中公司政策在调用间丢失、幻觉组织架构的问题,通过持续更新的知识库注入相关上下文,并处理权限边界和Token预算。
Almanac(YC S26)一问世就承诺解决困扰大多数多 Agent 金融系统的上下文持久化问题:Agent 在调用之间遗忘公司政策、虚构组织架构,或者重复提问同一个问题。他们的卖点是" Hermes 加上大脑",即一个能够维护一份自我更新的公司知识 Wiki,并在每次 LLM 调用时注入相关上下文的 Agent。
基础设施层面的挑战并不是检索增强生成(RAG)本身,而是构建一个能够决定向 Agent 注入哪些公司文档、在部门之间实施权限边界、处理政策变化时的上下文漂移、以及在公司上下文与同一提示窗口中的用户查询竞争时管理 Token 预算的系统。
大多数 Agent 系统将公司知识视为向量库中的静态 Embedding。你将文档分块、Embedding、检索 Top-K 匹配项,然后将它们塞进提示词。这在生产环境中会在三个地方出问题:
过时上下文:公司政策每天都在变化。Embedding 落后于现实,除非你持续重新索引。
权限泄露:朴素检索会拉取用户不应该看到的文档。你需要在查询时实施行级安全。
Token 预算崩塌:注入五页公司上下文后,实际用户查询或 Agent 推理就没有空间了。
Almanac 的做法是维护一个实时 Wiki,汇总来自已集成工具(Slack、Gmail、Granola 笔记、GitHub Issues)的活动,并实时更新页面。Agent 在每次操作前都会读取这个 Wiki。Wiki 不是缓存,而是事实来源。
系统有三层:
工具连接器:OAuth 集成,从 Slack 频道、邮件线程、日历邀请和项目管理工具中流式获取事件。每个事件都带有元数据标签(作者、时间戳、项目、客户名称)。
Wiki 编译器:一个后台进程,将相关事件分组到页面中。客户页面聚合所有关于该客户的 Slack 线程、邮件和会议笔记。项目页面汇编 GitHub Issues、Pull Request 和设计文档。编译器持续运行,不按时间表执行。
上下文选择器:当 Agent 收到任务("为 Vercel 起草续约提案")时,选择器会查询 Wiki 获取相关页面。它结合使用关键词匹配(从任务中提取实体)和语义搜索(Embedding 相似度)。选择器返回的是排名后的页面列表,而非原始文档。
然后 Agent 读取排名前三的页面,决定是否有足够上下文继续。如果没有,它会提出澄清问题或搜索更多页面。
最困难的部分是防止 Agent 在部门边界之间泄露敏感数据。Almanac 的权限模型有两个实施点:
摄入时:当 Wiki 编译器处理 Slack 消息或邮件时,它继承源工具的访问控制列表(ACL)。#finance-internal 频道中的消息会被标记上可以看到该 Slack 频道的用户列表。包含该消息的 Wiki 页面继承相同的 ACL。
检索时:当上下文选择器查询 Wiki 时,它会按发起 Agent 任务的用户过滤页面。如果用户无法查看源 Slack 频道或邮件线程,则该页面会被排除在结果之外。
这是文档级别的行级安全。这不是完美的。如果用户将敏感邮件转发到公共 Slack 频道,该 Wiki 页面就会对该频道中的所有人可见。系统不会尝试检测或阻止这种情况。它信任源工具的权限。
公司上下文与用户查询和 Agent 推理在同一个提示窗口中竞争空间。Almanac 使用分级预算(数值基于典型 LLM 约束和产品演示中观察到的行为估算):
如果排名前三的 Wiki 页面超过 3,000 个 Token,系统会使用单独的 LLM 调用对它们进行摘要。摘要保留关键事实(客户名称、续约日期、定价条款)但去掉对话性填充内容。这会增加 200-400ms 的延迟,但可以防止上下文截断。
如果 Agent 需要更多上下文,可以请求额外页面。这会触发第二次检索传递和新的 Token 预算计算。大多数任务在一到两次传递中解决。
当公司政策发生变化时,过时上下文会污染未来的 Agent 调用。Almanac 通过事件驱动更新来处理这个问题:
没有批量重新索引。每一次更改都会实时传播到 Wiki。这是因为 Wiki 很小(几百页,不是数百万文档)。整个 Wiki 可以放在一台服务器的内存中。
权衡在于一致性。如果两个用户同时编辑同一个 Slack 线程,Wiki 编译器按事件到达的顺序处理。最后一次写入获胜。这对于大多数金融工作流是可以接受的,因为冲突很少见,用户可以手动协调差异。
Almanac 作为有状态服务运行,包含三个组件:
事件摄入 Worker:每个已连接工具一个 Worker。每个 Worker 轮询该工具的 API 获取新事件,并将其写入 Postgres 队列。
Wiki 编译器:单线程进程,从队列中读取,将事件分组到页面,并写入 Wiki 存储(同样是 Postgres)。
Agent 运行时:处理用户任务的工作池。每个 Worker 查询 Wiki、调用 LLM、执行工具操作并将结果流式返回给用户。
Agent 运行时是无状态的。每个任务都是独立的。Wiki 编译器是有状态的,运行在单台服务器上以避免一致性问题。如果编译器崩溃,它从队列中最后一个已处理事件恢复。
系统不使用向量数据库。所有检索都在 Postgres 中进行,使用全文搜索(tsvector)和 pg_embedding 实现语义相似度。这保持了技术栈的简单性,避免了管理独立向量存储的运维复杂性。
权限漂移:如果用户被撤销了对 Slack 频道的访问权限,Wiki 编译器不会追溯删除包含该频道消息的页面。用户仍然可以看到历史上下文,直到页面因新事件而更新。
Token 预算溢出:如果用户提出需要五个 Wiki 页面的复杂问题,系统会对全部五个进行摘要。摘要可能会丢失关键细节。Agent 不会就此警告用户。
工具 API 速率限制:如果事件摄入 Worker 遇到速率限制,它会指数级退避。在退避期间,新事件不会被处理。Wiki 变得过时。Agent 不知道这一点,可能会返回过时的上下文。
上下文注入延迟:检索和摘要 Wiki 页面会给每次 Agent 调用增加 200-800ms 的延迟。对于高频任务(监控告警、实时交易信号),这种延迟是不可接受的。
def select_context(task: str, user_id: str, max_tokens: int = 3000) -> list[WikiPage]:
# Extract entities from task (customer names, project names)
entities = extract_entities(task)
# Keyword search for exact matches
keyword_results = db.execute(
"""
SELECT id, title, content, ts_rank(search_vector, query) as rank
FROM wiki_pages
WHERE search_vector @@ plainto_tsquery('english', :entities)
AND :user_id = ANY(acl)
ORDER BY rank DESC
LIMIT 10
""",
entities=" ".join(entities),
user_id=user_id
)
# Semantic search for related pages
task_embedding = embed(task)
semantic_results = db.execute(
"""
SELECT id, title, content, 1 - (embedding <=> :task_embedding) as similarity
FROM wiki_pages
WHERE :user_id = ANY(acl)
ORDER BY similarity DESC
LIMIT 10
""",
task_embedding=task_embedding,
user_id=user_id
)
# Merge and deduplicate results (combines keyword + semantic scores)
pages = merge_results(keyword_results, semantic_results, top_k=3)
# Summarize if total tokens exceed budget
total_tokens = sum(count_tokens(p.content) for p in pages)
if total_tokens > max_tokens:
pages = [summarize_page(p, max_tokens // len(pages)) for p in pages]
return pages
该查询结合了全文搜索(关键词匹配)和向量相似度(语义搜索)。两个查询都按用户的 ACL 过滤。结果被合并、去重,并在超过 Token 预算时进行摘要。
核心权衡:Almanac 选择上下文新鲜度和运维简单性,而非规模和亚秒级响应时间。
在以下情况使用 Almanac 的架构:
在以下情况避免这种方法:
该系统能够运作是因为它在简单性和规模之间做了取舍。单个 Postgres 实例、单个编译器线程、没有向量数据库。这对于大多数早期公司来说是正确的取舍。当你达到 1,000 名员工或 1,000 万份文档时,它就会失效。