十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Agno Result Offloading 实战:把巨型工具输出卸载到存储,终结长会话上下文爆炸

Agno Result Offloading 实战:把巨型工具输出卸载到存储,终结长会话上下文爆炸 Agno Result Offloading 实战把巨型工具输出卸载到存储终结长会话上下文爆炸【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno本篇技术指南讲解 Agno 框架中的 Result Offloading工具结果卸载机制当 Agent 的工具返回超过 16,000 字符的巨型结果时如何用offload_tool_results将完整 payload 写入数据库、在会话记录中只保留一个信封式指针并借助read_result/search_result两个回读工具按需取回内容。读完你将掌握ResultStore的全部配置项、SQLite / PostgreSQL / 本地磁盘三种落盘方案、过期清扫与删除级联以及该机制在源码层面的设计边界——适用于所有饱受长工具输出折磨的 Agent 生产场景。问题背景长 Agent 会话死于自己的工具输出Agno 官方文档在 22_result_offloading/README.md 中开宗明义地描述了这个痛点A long agentic run dies of its own tool output. One large search result sits in the message list forever, re-sent on every later model call.一次搜索工具返回 143KB 的结果这个结果会一直留在消息列表里在之后每一次模型调用中被原样重新发送。多轮对话下来上下文窗口被同一份巨型 payload 反复撑大token 成本线性增长最终上下文溢出导致整个 run 失败。Result Offloading 的解决方案是让会话记录只保存一个指向结果的指针而不是结果本身。核心机制信封Envelope替代 Payload在 01_offload_tool_results.py 中一个fetch_catalog工具返回了 4,000 行、约 143KB 的商品目录表格。开启卸载后这条 tool 消息在转录中被替换为一个短小的信封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).这个信封携带三部分信息元数据头result_id形如res_ 10 位哈希、工具名、行数、字节大小头部预览默认最多 20 行或 1,200 字符取先到者回读指引提示模型用read_result或search_result取回剩余内容。结果是Transcript 只增长不到 1KB而完整 143KB 的字节始终可恢复。根据该目录下的 TEST_LOG.md 记录实测中 146,300 字符的结果被替换为 920 字符的信封且Per-call input tokens stayed under 1,000 on every turn即巨型结果从未重新进入上下文。快速上手from 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 # 4,000 行、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, )运行方式使用仓库内的 demo 虚拟环境.venvs/demo/bin/python cookbook/02_agents/22_result_offloading/01_offload_tool_results.py开启条件只有两个offload_tool_resultsTrue以及一个支持卸载的数据库SqliteDb或PostgresDb详见下文数据库要求。在示例 01 中模型被引导使用search_result而非重新调用工具这是本机制的关键使用习惯——回读代替重取。两个回读工具read_result 与 search_result卸载后Agent 自动获得两个工具由 offload/tools.py 中的工厂函数get_read_result_function/get_search_result_function注入read_result(result_id, start_line1, end_lineNone, start_char0)按行号或行内字符偏移读取一个有限大小的分页。每一页回复都会指出下一页的起始行next_start_line从而支持把整个 payload 一页一页读完search_result(result_id, pattern, context_lines0)用正则表达式在完整 payload 内定位匹配行最多返回 20 个匹配每行截断到 500 字符可附带上下文行。每个回读都是封顶的bounded源码 offload/store.py 明确定义了所有上限常量值含义READ_MAX_LINES400read_result单页最多行数READ_MAX_CHARS16,000read_result单页最多字符数与默认阈值相等见下SEARCH_MAX_MATCHES20search_result最多返回匹配数SEARCH_MAX_CONTEXT_LINES20每个匹配最多附带上下文行数SEARCH_LINE_CLIP500匹配行截断长度SEARCH_TIMEOUT_SECONDS10.0可回溯正则的扫描超时值得注意的设计细节搜索使用 Pythonre而正则没有超时概念一个灾难性回溯catastrophic backtracking的模式可能永久挂起 run。源码针对这一点做了防御——只有包含* ? { (之一即存在重复构造、可能爆炸的模式才会被放进子进程扫描并在 10 秒后强制终止见 store.py不包含这些字符的模式必然线性扫描就地执行。整个搜索回复的总量被约束在一个read_result页内正如源码注释所言a search can never put back what offloading took out搜索永远不会把卸载省掉的量又塞回去。直接使用 ResultStore 的完整 API 演示02_result_store.py 展示了ResultStore脱离 Agent、独立作为存储引擎使用的方法每个方法都有a前缀的异步孪生版本from 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) ref store.offload( session_iddemo-session, run_idrun-1, tool_call_idcall-1, tool_namefetch_report, tool_args{station: all}, outputREPORT, ) print(ref.result_id, ref.size_bytes, ref.line_count, ref.content_type) # 读取一个有限页回复会指出下一页从哪行开始 page store.read(ref.result_id, start_line1, end_line5) print(page.text, next:, page.next_start_line, truncated:, page.truncated) # 搜索封顶 20 个匹配每行截断 matches store.search(ref.result_id, rstation N$) # 该会话现存的结果 id最新在前封顶 20 个 store.live_ids(demo-session) # 读完整份 payload 不断翻页直到 next_start_line 为 None start 1 while start is not None: page store.read(ref.result_id, start_linestart) start page.next_start_line # 清理删除索引行与 payload store.delete_for_sessions([demo-session])按 TEST_LOG.md 实测2,000 行 / 66,823 字节的报告在threshold_chars4000下被存储第一页返回第 1–5 行且next_start_line: 6搜索命中 20 个恰好是上限整份 payload 用 5 页read_result读完delete_for_sessions后live_ids归空。ResultStore改变默认行为的设置对象把offload_tool_resultsTrue换成传入一个ResultStore实例即可改变所有默认行为from agno.offload import ResultStore Agent(offload_tool_resultsResultStore(threshold_chars8000, ttl_seconds86400))对照源码 store.py 的__init__ResultStore的完整参数如下参数默认值说明threshold_chars16,000结果超过该字符数才卸载必须 ≥ 1否则抛ValueErrorpreview_lines20信封预览最多行数preview_chars1,200信封预览最多字符数与行数取先到者ttl_secondsNone过期时间由清扫sweep回收过期 payloadNone 表示存活到会话删除member_responsesTrue团队模式专用同时为成员自身的已存储 run 生成信封dbNone索引库默认取 Agent 的 dbfsNonepayload 的 AgentFS 后端默认是挂在 db 上的 AgentFS阈值为何默认是 16,000源码注释解释了这个看似任意的数字DEFAULT_THRESHOLD_CHARS READ_MAX_CHARSstore.py——阈值等于一次read_result的整页读取量。低于这个大小把一个结果存下来再读回一页的成本比当初直接内联保留还高。换言之卸载只在存下来 按需读回确实更省时才有意义。设置对象是只读复制的你传入的ResultStore实例永远不会被修改。Agent 在启动时会通过bound()方法store.py把它绑定到自己的数据库上并生成一份副本这份副本暴露为agent.result_store。因此同一个ResultStore设置对象可以配置多个 Agent——每个 Agent 拿到的是各自的绑定副本。自定义设置与过期清扫实战04_custom_store_settings.py 演示了完整流程settings ResultStore( threshold_chars4000, # 更激进的卸载阈值 preview_lines3, # 信封只预览 3 行 preview_chars200, # ... 或 200 字符 ttl_seconds3600, # 1 小时生命周期 ) agent Agent( modelOpenAIResponses(idgpt-5.5), dbdb, tools[count_inventory], offload_tool_resultssettings, # 传入设置对象而非 True markdownTrue, ) store agent.result_store # 绑定副本 print(store is settings) # False不是同一个对象 print(store.threshold_chars, store.ttl_seconds, store.db is db)关于生命周期ttl_seconds会给索引行盖上过期时间戳expires_at由清扫任务回收。清扫在每次写入时自动触发但频率最高不超过每 5 分钟一次源码常量SWEEP_INTERVAL_SECONDS 300且更短的 TTL 会按其自身长度触发见 store.py。示例中手工调用store.sweep_expired(nowint(time.time()) 3660)模拟一小时零一分之后验证了清扫确实移除过期结果、live_ids随即清空。三个落点结果到底存在哪里一次卸载的结果落在三处而其中只有一处属于会话03_where_results_live.py会话行session row只保存信封——一段预览加一个result_id。这是模型此后唯一能看到的、也是后续轮次唯一会被重发的内容AgentFSagno_fs完整文本进入 Agent 的文件存储。SQLite 下是同一数据库文件里的agno_fs表索引行agno_tool_results每个结果一行记录 payload 的命名空间namespace与路径、所属 session 与 run、大小、行数、预览和可选过期时间。关键点卸载是无损的。该示例用一个 tool hook 验证了这一点——hook 运行在工具周围看到的是完整 87,223 字符的结果seen_by_hook[read_access_log] 87223而模型看到的、以及RunOutput.tools[0].result中保存的都是 1,315 字符的信封。正如 README.md 所言Nothing is lost on the way——只有模型读取的东西发生了变化hook、指标和你的业务代码看到的仍然是完整结果。示例最后用纯 SQL 统计了三处落点agno_sessions1 行、agno_tool_results1 行、agno_fs1 行 87,223 字节与预期完全一致。索引行的 namespace 格式为tool-results/session_id-哈希payload 路径为results/run_id/res_哈希.txt。绝不会被卸载的内容README 明确列出了一组绝不卸载的例外与源码 runs.py 和 types.py 中的NEVER_OFFLOADED_TOOLS (read_result, search_result)相互印证失败的 tool 调用模型需要逐字看到错误文本才能自我修正低于阈值的结果内联保留更划算read_result/search_result自己的输出它们本身就是回读工具输出已封顶卸载它们是死循环终结 run 的那个结果不再有后续调用需要重发它媒体文件只替换消息文本图片、视频、音频和文件原样通过不受影响。失败是响亮的绝不静默如果写入被拒绝或后端报错卸载不会假装成功。README 明确承诺Failure is loud, never silent. 此时信封会明确说明写入失败并携带头部 尾部预览_TAIL_LINES 5见 store.py代替指针run 继续执行模型依然能获得关键信息。数据库要求与存储布局卸载功能要求数据库为SqliteDb或PostgresDbpayload 通过同步文件系统后端落盘。任何其他数据库下该设置被当作关闭处理并会打印一条点名该数据库的警告。数据的具体去向会话行只保存信封完整文本进入AgentFSagno_fsSQLite 下是与会话同一个 SQLite 文件的agno_fs表PostgreSQL 下是fsschema被该数据库内所有db_schema共享每个结果一行索引进入agno_tool_results位于你的 schema 内记录 payload 的 namespace 与路径、大小、预览和可选过期时间删除会话会级联删除它的索引行和 payload。ResultStore的fs参数可以把 payload 指向任意 AgentFS 后端。配额从源码可推断store.py 定义了三个存储上限可以推断它们是 AgentFS 默认值之上为本 store 抬高的配额常量值MAX_RESULT_BYTES8,000,000单个结果约 8MBMAX_SESSION_NAMESPACE_BYTES200,000,000单个会话命名空间约 200MBMAX_CALL_ID_ATTEMPTS1,000超限会触发QuotaExceededError路径落入响亮失败分支。PostgreSQL 布局双 schema 并存在 PostgreSQL 上06_postgres_layout.py 展示了两张表分居两个 schema 的布局agno_tool_results索引建在你的db_schema中与 session 表为邻因此一个数据库可以并排承载多个应用agno_fsAgentFS payload 表建在共享的fsschema中为该数据库所有db_schema共用。每个 payload 的 namespace 因此带上 schema 名如tool-results/postgres-layout-4e329620两个复用相同 session id 的应用绝不会共享 payload 行。先启动数据库./cookbook/scripts/run_pgvector.sh示例连接串为postgresqlpsycopg://ai:ailocalhost:5532/ai随后分别查询两个 schemaSELECT 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/postgres-layout-%;TEST_LOG.md 实测索引行出现在ai.agno_tool_resultspayload 出现在fs.agno_fs47,249 字节的结果在会话记录中只有 821 字符delete_session后fs.agno_fs残留 0 行。payload 落本地磁盘fs 参数默认 payload 落在 Agent 数据库上的 AgentFS。fs可以把 payload 指向任何 AgentFS 后端本地目录、另一张表、另一个数据库。索引行无论如何都留在 Agent 库的agno_tool_results中并携带 namespace 和 path因此回读工具和会话删除级联都能找到它们。05_payloads_on_disk.py 展示了本地目录方案from agno.fs import FileSystem from agno.fs.local import LocalFileSystem from agno.offload import ResultStore PAYLOAD_DIR tmp/offloaded_payloads agent Agent( modelOpenAIResponses(idgpt-5.5), dbdb, tools[load_shipping_manifest], offload_tool_resultsResultStore( fsFileSystem(backendLocalFileSystem(rootPAYLOAD_DIR)) ), markdownTrue, )运行后索引行指向tool-results/payloads-on-disk-.../results/run_id/res_hash.txt磁盘上出现对应的 35,599 字节文件db.delete_session()同时移除索引行与文件。一个需要知道的边界README 明确提示一个从未用该fs构建过 store 的进程例如单独的清理脚本在删除会话时无法触达这些文件——它会移除索引行并明确记录log哪些 payload 路径未能触达。这提醒我们清理逻辑应当复用创建时的 store 配置。删除级联与用户隔离存储的结果从属于它的会话。07_delete_session_cascade.py 演示了delete_session/delete_sessions的删除语义删除范围被严格限定为这次删除被允许移除的内容。# 错误用户什么都不发生 deleted db.delete_session(session_idtickets-alice, user_idbob) # - Falsealice 的索引行和 payload 原样保留 # 正确用户会话、索引行、payload 一起消失 deleted db.delete_session(session_idtickets-alice, user_idalice) # - True两个用户alice / bob、两个会话的实测结果错误的用户删除返回False且不留任何痕迹正确用户删除返回True并级联移除索引行与 payload同时 bob 的结果毫发无损。一个针对某用户 id 的删除即使写入了别人的 session id也绝不会触碰别人的 payload。与团队模式的联动团队成员的回答以同样的方式被卸载member_responsesTrue默认开启覆盖成员自身已存储的 run而成员答案作为工具结果传给 leader 时本就会走卸载路径。团队场景的完整示例见 03_teams/27_result_offloading/。生产建议基于以上机制与实测把 Result Offloading 用于生产时可以遵循几条原则开启即受益任何工具可能返回超长文本的 Agent 都应考虑offload_tool_resultsTrue它同时满足无损full bytes recoverable、零写路径模型调用no model call on the write path、回读封顶every read back is capped三个硬性保证引导模型回读而非重取提示词中明确要求使用search_result定位、read_result按行读取而不是重新调用原始工具示例 01 的 prompt 即如此设计按场景调阈值结果普遍偏小则不必降低阈值threshold_chars语义是超过多少才值得存低于 16,000 的结果存下来反而更贵善用 TTL有明确时效的数据日志、临时报表设置ttl_seconds让清扫自动回收需长期保留则留空随会话删除级联清理清理脚本与 store 配置保持一致使用自定义fs时删除逻辑必须由知道这个 fs的进程执行否则 payload 文件无法被级联触达数据库选型SQLite 适合单机快速验证PostgreSQL 的多 schema 布局自己的db_schema 共享fsschema适合多应用共库的生产环境。深入阅读机制总览与参数表格cookbook/02_agents/22_result_offloading/README.md7 个循序渐进示例从最小开启到删除级联cookbook/02_agents/22_result_offloading/ 目录下的01_至07_脚本实测记录gpt-5.5 / SQLite / PostgreSQL 全 PASScookbook/02_agents/22_result_offloading/TEST_LOG.mdResultStore核心实现与全部上限常量libs/agno/agno/offload/store.py回读工具工厂与信封提示词libs/agno/agno/offload/tools.py结果类型定义ResultRef/ResultPage/ResultMatch/NEVER_OFFLOADED_TOOLSlibs/agno/agno/offload/types.py卸载在 Agent 启动时的接线逻辑与compress_tool_results互斥等libs/agno/agno/agent/_init.py团队场景对应教程cookbook/03_teams/27_result_offloading/【免费下载链接】agnoBuild, run, and manage agent platforms.项目地址: https://gitcode.com/GitHub_Trending/ag/agno创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表