agno Agent 结果卸载(Result Offloading):用 ResultStore 与 read_result/search_result 驯服超大工具输出

agno Agent 结果卸载(Result Offloading):用 ResultStore 与 read_result/search_result 驯服超大工具输出 agno Agent 结果卸载Result Offloading用 ResultStore 与 read_result/search_result 驯服超大工具输出【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno本篇指南以 agno 仓库cookbook/02_agents/22_result_offloading目录下的示例与测试记录为主体完整讲解Agent(offload_tool_resultsTrue)与ResultStore的机制、配置与落地实践。读完你将掌握如何让一次工具调用返回的数十万字符结果不再反复灌入模型上下文如何用信封envelope、read_result/search_result按需回读如何用ResultStore自定义阈值、预览与 TTL以及结果在数据库、索引表与 AgentFS 三个位置的真实布局和删除级联行为。问题背景长 Agent 运行死于自己的工具输出一次长 Agent 运行的最大威胁往往来自工具本身。一个搜索类工具一次性返回几千行、十几万字符的表数据这条巨型消息会永久停留在消息列表中之后每一轮模型调用都会被重新发送一遍。上下文被逐步撑爆token 成本与延迟同步上升最终运行以失败告终。agno 的解决思路非常直接转录本transcript里保存指针而不是负载。开启offload_tool_resultsTrue后超过阈值的工具结果被写入数据库消息里只留下一个包含预览与结果 ID 的短信封Agent 额外获得read_result与search_result两个工具可以按需回去取剩余内容。在 01_offload_tool_results.py 的实测中一个 4,000 行、146,300 字符的目录表被替换为 920 字符的信封lines4000 size142.9KB模型通过search_result(^SKU-00042\b)精确定位到目标行并正确作答全程每轮输入 token 保持在 1,000 以下143KB 的结果从未重新进入上下文。核心机制offload_tool_results 与信封替换在 README.md 中官方给出了信封的完整形态result idres_a91c4f20b3 toolfetch_catalog lines4000 size142.9KB {first 20 lines / 1200 chars of the result} /result Full result stored; read with read_result(res_a91c4f20b3) or search_result(res_a91c4f20b3, pattern).默认行为可以概括为几条硬规则阈值长度超过 16,000 字符的结果才被卸载。这个数字并非随意设定从源码 store.py 可见READ_MAX_CHARS 16_000且DEFAULT_THRESHOLD_CHARS READ_MAX_CHARS——16,000 恰好是read_result单页回读的上限因此低于阈值的结果回读成本反而高于原地保留的成本。预览信封默认展示前 20 行DEFAULT_PREVIEW_LINES 20或前 1,200 字符DEFAULT_PREVIEW_CHARS 1200以先到者为准保证模型能看到结果的头部内容。零抽象损耗没有任何内容被摘要化、丢弃或改写写入路径上不发起任何模型调用所有回读read_result、search_result都有严格上限。替换时机替换发生在工具消息构建之前因此持久化的会话行里保存的同样只是信封——后续会话恢复或继续时模型看到的依然是信封而非负载03_where_results_live.py 中用 SQL 验证了这一点。底层回读上限源码级源码 store.py 定义了所有回读的硬性约束理解这些数字有助于把握该功能在生产中的行为边界常量值作用READ_MAX_LINES/READ_MAX_CHARS400 行 / 16,000 字符read_result单页上限先到者生效翻页靠next_start_lineSEARCH_MAX_MATCHES20search_result最多返回的匹配数SEARCH_MAX_CONTEXT_LINES20每个匹配可带的上下文行数上限SEARCH_LINE_CLIP500单行匹配文本的裁剪长度SEARCH_TIMEOUT_SECONDS10.0可回溯的正则模式在子进程中执行超时被杀死防止灾难性回溯挂死运行MAX_RESULT_BYTES8,000,000单结果字节配额MAX_SESSION_NAMESPACE_BYTES200,000,000单会话命名空间总字节配额SWEEP_INTERVAL_SECONDS300TTL 清理两次执行的最小间隔正则搜索的安全性处理值得一提模式由模型提供而 Python 的re没有超时_BACKTRACKING_CHARS frozenset(*?{()判断模式是否可能回溯只有含这些字符的可回溯模式才被送入带截止时间的子进程普通模式在进程内线性扫描。快速上手一行代码开启卸载最小化开启方式是在Agent上同时提供db与offload_tool_resultsTruefrom agno.agent import Agent from agno.db.sqlite import SqliteDb from agno.models.openai import OpenAIResponses def fetch_catalog() - str: Fetch the full parts catalog as a tab-separated table. return CATALOG # 4000 行、约 143KB 的字符串 agent Agent( modelOpenAIResponses(idgpt-5.5), dbSqliteDb(db_filetmp/offloading.db), tools[fetch_catalog], offload_tool_resultsTrue, markdownTrue, ) output agent.run( Fetch the catalog, then tell me which warehouse SKU-00042 is in and how many rows the catalog has. Use search_result rather than re-fetching., session_idoffloading-basic, )完整可运行示例见 01_offload_tool_results.py。该示例还在运行后打印了转录本中工具消息的实际长度与原始结果长度对比直观展示信封替换效果for message in output.messages or []: if message.role tool: print(ftool message length: {len(str(message.content))} chars (the full result was {len(CATALOG)})) print(str(message.content)[:400]) break测试记录 TEST_LOG.md 显示该示例针对gpt-5.5OpenAIResponses与 SQLite 实测 PASS模型答对 SKU-00042 位于 C 仓4,000 行数据取模 5 得到 C、目录共 4,000 行两次连续运行结果完全一致无警告、无 traceback。运行方式仓库内.venvs/demo/bin/python cookbook/02_agents/22_result_offloading/01_offload_tool_results.pyResultStore可独立使用的设置对象ResultStore既是被传给 Agent 的设置对象也是一个不依赖 Agent 就能直接使用的存储工具。README 给出了切换默认值的写法from agno.offload import ResultStore Agent(offload_tool_resultsResultStore(threshold_chars8000, ttl_seconds86400))完整的构造参数来自 04_custom_store_settings.py 与源码 store.pyResultStore( threshold_chars16000, # 超过此字符数的结果才被存储 preview_lines20, # 信封最多展示的行数 ... preview_chars1200, # ... 或字符数取更小者 ttl_secondsNone, # 过期时间sweep 会回收过期负载 member_responsesTrue, # 仅团队模式成员自己的存储 run 也做信封化 dbNone, # 默认取 Agent 的 db fsNone, # 负载的 FileSystem默认是 db 上的 AgentFS )两点关键设计值得注意传入对象永不修改。ResultStore.bound(db)返回一个绑定到指定数据库的副本这个副本挂在agent.result_store上因此同一个设置对象可以配置多个不同数据库上的 Agent源码 store.py。threshold_chars至少为 1否则构造时直接抛ValueErrorstore.py。直接使用 ResultStore 的完整 API02_result_store.py 演示了不经过 Agent、直接操作存储的全部环节每个方法都有a前缀的异步孪生如aoffload、aread、asearch、alive_ids、asweep_expired、adelete_for_sessionsfrom agno.db.sqlite import SqliteDb from agno.offload import ResultStore db SqliteDb(db_filetmp/offloading_store.db) store ResultStore(dbdb, threshold_chars4000, ttl_seconds7 * 86400) # 1) 卸载写入 2000 行报告得到 result_id、字节数、行数、content_type ref store.offload( session_iddemo-session, run_idrun-1, tool_call_idcall-1, tool_namefetch_report, tool_args{station: all}, outputREPORT, ) # 2) 有界分页回读每页会告诉你下一页从哪一行开始 page store.read(ref.result_id, start_line1, end_line5) print(page.text, page.next_start_line, page.truncated) # 3) 搜索上限 20 个匹配每行被裁剪 matches store.search(ref.result_id, rstation N$) # 4) 列出该会话现存结果 id最新在前上限 20 store.live_ids(demo-session) # 5) 翻完整页循环直到 next_start_line 为 None pages, start 0, 1 while start is not None: page store.read(ref.result_id, start_linestart) pages 1 start page.next_start_line # 6) 清理删除索引行与负载 store.delete_for_sessions([demo-session])测试记录中这段流程的实测结果非常具体写入 2,000 行 66,823 字节的文本第一页返回 1–5 行且next_start_line: 6搜索返回 20 个匹配达上限首个在第 4 行live_ids列出唯一结果整份负载需要 5 次read_result翻完delete_for_sessions返回 1随后live_ids为空。全程无警告、无 traceback。自定义设置与 TTL 清理的实测04_custom_store_settings.py 将阈值降到 4,000、预览缩到 3 行 / 200 字符、TTL 设为 1 小时并手动触发过期清理settings ResultStore( threshold_chars4000, preview_lines3, preview_chars200, ttl_seconds3600, ) agent Agent(..., offload_tool_resultssettings, markdownTrue) # 运行后检查绑定副本 store agent.result_store print(store is settings) # Falsesettings 未被修改得到的是绑定副本 print(store.threshold_chars) # 4000 print(store.ttl_seconds) # 3600 # 过期清理sweep 平时在写入时自动运行间隔至少 5 分钟此处手动模拟 1 小时 1 分钟后 swept store.sweep_expired(nowint(time.time()) 3660)测试记录确认模型正确算出 item-7 的 329 单位按i % 53 7对货位求和为 329信封精确预览 3 行agent.result_store is settings打印 False且副本的 threshold 为 4000、ttl 为 3600、db 已绑定expires_at - created_at为 3600 秒以“当前时间 3660 秒”执行清理后删除了 1 条结果live_ids为空。结果落盘的三个位置一个被卸载的结果会同时出现在三个地方且只有一处属于会话README.md会话行只保存信封预览 result_id。这是模型此后能看到并反复重发的全部内容。AgentFS 负载完整文本进入 AgentFS 文件存储。SQLite 下是同一数据库文件中的agno_fs表PostgreSQL 下是共享的fsschema 中的agno_fs表。索引行每个结果在agno_tool_results表中占一行记录负载的 namespace 与 path、所属 session/run、大小、预览及可选过期时间。03_where_results_live.py 用一个返回 1,500 行87,223 字符访问日志的 Agent 完整验证了这三处# 1. 会话行里是信封 stored_run session.runs[-1] for message in stored_run.messages or []: if message.role tool and message.tool_name read_access_log: print(len(str(message.content))) # 1315 字符而非 87223 print(len(str(output.tools[0].result))) # RunOutput.tools[0].result 同样是信封 # 2. 索引行 rows db.get_tool_results_for_session(session_id) for key in (result_id, namespace, path, session_id, run_id, tool_name, size_bytes, line_count): print(key, rows[0][key]) # 3. AgentFS 负载可原样读回 payload FileSystem(backenddb, namespacerows[0][namespace]).read(rows[0][path]) print(payload ACCESS_LOG) # True与工具原始输出逐字节一致 # 用原生 SQL 数一遍三个表 # SELECT count(*) FROM agno_sessions WHERE session_id ... # SELECT count(*) FROM agno_tool_results WHERE session_id ... # SELECT count(*), sum(size_bytes) FROM agno_fs WHERE namespace ...测试记录给出的实测细节模型通过search_result找到了唯一的 500GET /api/orders/777存储的工具消息与RunOutput.tools[0].result都是 1,315 字符的信封而工具钩子tool hook看到的是全部 87,223 字符——钩子、指标和你自己的代码不在信封这一侧它们永远能看到完整结果只有模型读取的内容被替换索引行为namespacetool-results/where-results-live-ec946760、pathresults/run_id/res_1f66b7d105.txt、87,223 字节、1,500 行FileSystem(backenddb, namespace...).read(path)返回与工具输出完全一致的负载SQL 统计为 1 条会话行、1 条索引行、1 条 87,223 字节的agno_fs行。永远不被卸载的内容不是所有工具结果都适合走卸载路径agno 明确划定了边界README.md失败的工具调用模型需要逐字的错误文本来自我修正不能替换成信封低于阈值的结果回读成本高于原地保留read_result/search_result自身的输出避免递归卸载结束运行的结果运行即将结束没有后续轮次需要保护上下文媒体只替换消息文本图片、视频、音频和文件原样透传不做任何处理。失败是响亮而非静默的写入路径上的失败有专门的处理策略如果写入被拒绝或后端报错信封不会静默降级成“看似正常”的内容而是明确说明失败原因并携带头部与尾部预览head and tail preview代替指针运行继续。这样模型既能感知存储失败又不至于丢失全部上下文信息。配额方面源码为每个结果与每个会话命名空间分别设置了MAX_RESULT_BYTES 8_000_000与MAX_SESSION_NAMESPACE_BYTES 200_000_000的上限超出即抛QuotaExceededError由_quota_reason转成信封中的可读说明。数据库要求与兼容性边界卸载功能依赖数据库提供会话与负载存储支持 SQLiteSqliteDb与 PostgreSQLPostgresDb存储负载走同步文件系统后端。其他任何数据库上该设置被当作关闭处理并输出一条指明数据库名称的警告而不是悄悄失败。PostgreSQL 的双 schema 布局06_postgres_layout.py 展示了 PostgreSQL 下两张表分处两个 schema 的布局agno_tool_results索引创建在你的db_schema中与会话同处因此一个数据库可以横向容纳多个应用agno_fsAgentFS 负载表创建在共享的fsschema 中被该数据库的所有db_schema共用每个负载的 namespace 因而携带 schema 名两个复用同一 session id 的应用永远不会共享负载行。db_url postgresqlpsycopg://ai:ailocalhost:5532/ai db PostgresDb(db_urldb_url, db_schemaai) agent Agent(..., dbdb, offload_tool_resultsTrue, markdownTrue) # 查询两个 schema # SELECT result_id, namespace, path, size_bytes FROM ai.agno_tool_results WHERE session_id :s # SELECT namespace, path, size_bytes, version FROM fs.agno_fs WHERE namespace LIKE tool-results/...%运行前需要先启动数据库./cookbook/scripts/run_pgvector.sh测试记录2026-08-21SQLite 与 PostgreSQL/pgvector 5532 端口显示模型正确回答 1,500 名员工中远程销售 75 人每 20 人取 1索引行出现在ai.agno_tool_results、负载行出现在fs.agno_fs两者 namespace 均为tool-results/postgres-layout-4e329620且 path 一致、47,249 字节、version 147,249 字符的结果在会话中仅存 821 字符的信封delete_session后fs.agno_fs中负载行归零。把负载放到本地磁盘fs 参数默认负载进数据库上的 AgentFSResultStore(fs...)可以把负载指向任意 AgentFS 后端——本地目录、另一张表、另一个数据库均可索引行始终留在 Agent 数据库的agno_tool_results中并携带 namespace 与 path因此回读工具和会话删除级联都能找到负载05_payloads_on_disk.pyfrom agno.fs import FileSystem from agno.fs.local import LocalFileSystem agent Agent( modelOpenAIResponses(idgpt-5.5), dbdb, tools[load_shipping_manifest], offload_tool_resultsResultStore( fsFileSystem(backendLocalFileSystem(roottmp/offloaded_payloads)) ), markdownTrue, )一个需要知道的坑如果某个进程从未用这个fs构建过 store例如单独的清理脚本它在删除会话时将无法触达这些文件——它会删除索引行并精确记录哪些负载路径无法触达。因此使用自定义fs时负责清理的进程也必须以相同配置构建 store。测试记录中的实测模型正确回答 1,200 行清单中需要冷库的有 400 行每三行取一索引行指向tool-results/payloads-on-disk-.../results/run_id/res_985546ab42.txt且tmp/offloaded_payloads/tool-results%2fpayloads-on-disk-.../results/run_id/下出现了 35,599 字节的真实文件delete_session同时移除索引行与磁盘文件目录下变为(no files)。删除级联与用户隔离卸载的结果归属于它的会话。delete_session/delete_sessions在删除会话的同时删除其索引行与负载且删除范围受用户权限约束为某个用户执行的删除绝不会触碰另一个用户的负载即使传入了对方的 session id07_delete_session_cascade.pyfor user_id, session_id in ((alice, tickets-alice), (bob, tickets-bob)): output agent.run( How many P1 tickets are open? Use search_result., session_idsession_id, user_iduser_id, ) # 错误用户删除什么都不会发生 deleted db.delete_session(session_idtickets-alice, user_idbob) # False # 正确用户删除会话、索引行、负载一起移除 deleted db.delete_session(session_idtickets-alice, user_idalice) # True测试记录中的实测alice 与 bob 各自会话均有 1 条索引行与 1 个负载以 bob 身份删除 alice 的会话返回 Falsealice 的索引与负载原封不动以 alice 身份删除返回 True索引与负载全部移除而 bob 的 1 条索引、1 个负载保持完好。两个用户的运行都正确回答了 1,200 张工单中的 12 张 P1每 97 张取 1。团队Team中的结果卸载团队成员的答案以同样的方式卸载成员回答到达 leader 时本身作为工具结果无论是否开启都会被处理ResultStore(member_responsesTrue)默认开启进一步覆盖成员自身存储 run 的信封化。团队内成员 store 只保留设置负载统一写入团队的数据库以便整个团队都能读取。相关示例见 cookbook/03_teams/27_result_offloading。验证与回归状态目录下的 TEST_LOG.md 记录了完整的功能验证结果2026-08-20gpt-5.5OpenAIResponses SQLite01_offload_tool_results.py与02_result_store.py均 PASS2026-08-21gpt-5.5 SQLite 与 PostgreSQLpgvector 容器端口 553203–07五个示例全部 PASS。七个示例全部给出精确的数据级结论信封长度、匹配数、翻页数、删除返回值等无警告、无 traceback。这份测试记录本身就是把卸载机制当作契约来对待的证据每一处“信封替代负载”“搜索封顶 20 条”“删除级联”的行为都被数字化验证可作为自行验证功能的对照基准。小结与选型建议凡是工具可能返回大文本、而 Agent 需要多轮交互的场景都值得开启offload_tool_resultsTrue单次read_result页大小即默认阈值 16,000 字符通常无需调整。需要精细控制预览、TTL 或负载落点数据库 vs 本地磁盘时传入自定义ResultStore注意同一设置对象可复用给多个 Agent且删除清理进程必须使用与写入进程一致的fs配置。生产环境建议明确ttl_seconds并依赖自动 sweep间隔至少 5 分钟回收过期负载同时利用 SQLite/PostgreSQL 下agno_tool_results索引与agno_fs负载表直接做运维审计。【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考