
AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆【免费下载链接】OperitThe most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent项目地址https://gitcode.com/gh_mirrors/op/Operit点击查看免费下载导读本文基于 Operit 仓库中docs/TODO/tool-result-per-call-limit_20260909/index.md的改造记录深入讲解一条影响 AI Agent 多工具并行调用质量的工程细节把整轮工具结果统一 64,000 字符上限改为单个 tool_result 独立限长、整轮全量保留。文章将结合源码 ConversationMarkupManager.kt、ToolExecutionLimits.kt 与回归测试讲清问题成因、改动边界、实现原理与验证方法帮助读者理解在长上下文 Agent 系统中如何平衡结果完整性与上下文长度。原本状况整轮 64,000 字符的先到先得困局改造之前Operit 的对话链路对工具结果采取了两层限制单个工具结果有长度限制由ToolExecutionLimits.MAX_SINGLE_TOOL_RESULT_MESSAGE_CHARS控制值为MAX_FILE_READ_BYTES * 2 64,000见 ToolExecutionLimits.kt整轮工具结果又叠加了一层统一的 64,000 字符上限。问题出在第二层当 Agent 在一轮中并行调用多个工具例如同时调用read_file、http_request、web_search时所有工具结果会拼接到一条用户消息里回传给模型。在整轮统一上限的约束下前面工具的大结果会先占满整轮预算排在后面的工具结果被直接截断丢弃历史转换层只能把它们标记为缺失unmatched。从源码结构看这种丢弃会进一步污染后续链路多个 Provider 的历史转换器如 ClaudeProvider.kt、OpenAIProvider.kt、DeepseekProvider.kt 等都内置了未匹配 tool_result的兜底逻辑tool_result_partial_batch、tool_result_without_structured_match用于在结果与调用对不上时把散落的工具调用刷成独立历史条目。这虽然保证历史不崩溃但代价是模型丢失了后续工具的实际输出——例如搜到了内容却因预算被占满而看不到结果。修改意图只限单个结果整轮全量保留本次改造的核心意图非常明确见 index.md只限制单个tool_result的序列化长度整轮保留所有工具结果确保每个工具调用都有对应的结果槽位。整体上下文长度继续由 token 统计和总结流程处理。也就是说长度治理的责任边界被重新划分字符级限制只作用于单个工具结果防止单条结果无限膨胀撑爆一次请求上下文总量控制不再由本轮结果拼接承担而是交给已有的 token 统计与上下文总结summarization机制在更宏观的尺度上管理整个对话窗口。这套划分符合结果槽位与调用一一对应的原则只要模型发出了 N 个 tool_use就必须收到 N 个完整的 tool_result否则并行调用的后半段结果永远有调用、无结果。作用域与改动边界原文档将改动收敛为四个点删除整轮工具结果的字符上限——不再对results.joinToString(...)拼接后的整条消息做统一截断将限制常量明确为单个工具结果上限——语义从整轮收敛到单条限制图片链接附加内容避免单个结果再次突破上限——防止图片链接以追加方式绕过单条限制增加多工具结果保留的回归覆盖——用测试锁定每条结果都被保留且各自限长的行为。对照源码改动落在 ConversationMarkupManager.kt 的buildToolResultMessage(results: ListToolResult): String它逐条调用formatToolResultForMessage再以换行拼接整个批次不再有任何整体长度上限。方法注释也明确写道Each result is bounded independently ...; the batch itself is not bounded because every tool call must retain a corresponding result slot.进度状态原文档标记本任务为implementation-complete四项子任务移除整轮截断、保留单个截断与 XML 完整性、增加回归测试、依照仓库约束不执行构建/测试命令均已完成。文档 frontmatter 中的status: implementation-complete表明这是已落地实现而非设计草案下文源码与测试即为佐证。实现原理单个结果如何独立限长常量定义与取值核心常量定义于 ToolExecutionLimits.ktobject ToolExecutionLimits { const val MAX_FILE_READ_BYTES 32_000 const val DEFAULT_FILE_READ_PART_LINES 200 const val MAX_TEXT_RESULT_LENGTH 5_000 /** Maximum serialized size of one tool result sent back to the model. */ const val MAX_SINGLE_TOOL_RESULT_MESSAGE_CHARS MAX_FILE_READ_BYTES * 2 }关键点MAX_SINGLE_TOOL_RESULT_MESSAGE_CHARS 32_000 * 2 64_000数值上与原整轮上限一致但语义从整轮共享预算变为每一条结果的独立预算。同文件还提供了MAX_TEXT_RESULT_LENGTH 5_000文本型结果的常规截断阈值与MAX_FILE_READ_BYTES 32_000文件读取字节上限两个配套常量共同构成工具执行的限额体系。单条限长与 XML 完整性formatToolResultForMessage负责把单条ToolResult序列化为 XML其核心是createBoundedToolResultXmlConversationMarkupManager.ktval emptyXml createToolResultXml( toolName toolName, status status, content bodyBuilder() ) val maxPayloadChars (ToolExecutionLimits.MAX_SINGLE_TOOL_RESULT_MESSAGE_CHARS - emptyXml.length) .coerceAtLeast(0) val boundedPayload truncatePayload(rawPayload, maxPayloadChars) return createToolResultXml( toolName toolName, status status, content bodyBuilder(boundedPayload) )这段实现有三个值得注意的工程细节限额是整条序列化结果而非纯 payload先构造空 content的 XML 骨架用MAX_SINGLE_TOOL_RESULT_MESSAGE_CHARS减去骨架长度得出 payload 可用的字符预算保证最终序列化消息含标签、属性不会超过 64,000 字符coerceAtLeast(0)兜底即使骨架本身异常长极端情况预算也不会变成负数导致崩溃截断时保留 XML 闭合结构truncatePayload在截断后追加\n[工具结果过长已截断]后缀TOOL_RESULT_TRUNCATION_SUFFIX见 ConversationMarkupManager.kt且始终以完整闭合的/tag结尾确保模型拿到的每条结果都是结构完整的 XML不会因为截断产生半截标签污染整条历史。另外XML 的标签名由ChatMarkupRegex.generateRandomToolResultTagName()随机生成形如tool_result_Xy9Z ...测试 ToolExecutionManagerMarkupTest.kt 中即有tool_result_Xy9Z、tool_result_Qw7K这类多标签并存、各自闭合的断言确保随机标签名不影响解析。图片链接的独立预算防止绕道上限工具结果中常携带多模态图片链接如link typeimage id.../link。为避免图片链接在文本截断后以追加方式再次撑爆单条上限实现专门做了两步处理splitImageLinksForModelConversationMarkupManager.kt先用MediaLinkParser从原始 payload 中剥离图片链接文本部分替换为 Image attached as multimodal input. 提示语appendImageLinksWithinResultLimitConversationMarkupManager.kt图片链接逐条回填每追加一条都先计算availableChars MAX_SINGLE_TOOL_RESULT_MESSAGE_CHARS - toolResultXml.length - 1超出剩余预算的行直接跳过returnforEach从而保证文本 图片链接的最终拼接仍落在单条 64,000 字符之内。这条正是原文档作用域第 3 条限制图片链接附加内容避免单个结果再次突破上限的落地实现。调用链工具结果如何汇入对话从调用链看buildToolResultMessage是工具执行完成后把结果回灌给模型的总出口EnhancedAIService.kt 中processToolResults一类的入口以toolResultMessageOverride ?: ConversationMarkupManager.buildToolResultMessage(results)生成整轮结果消息若拼接结果为空则直接跳过本次 AI 请求并记录告警日志避免把空消息发给模型结果消息随后进入各 Provider 的历史转换层以 XMLtool_result形式交给 OpenAIProvider.kt转为role: tool消息、ClaudeProvider.kt转为tool_resultcontent 块、GeminiProvider.kt转为FunctionResponse等。因此整轮全量保留直接意味着每个 Provider 历史里都能找到与每个 tool_use 对应的 tool_result从根本上减少tool_result_partial_batch/tool_result_without_structured_match这类兜底路径的触发频率相关兜底逻辑见 StructuredToolCallBridge.kt。回归测试锁定每条结果都被保留本次改造新增的回归测试位于 ConversationMarkupManagerResultLimitTest.ktTest fun keeps every tool result while bounding each result independently() { val results listOf( ToolResult(first_tool, true, StringResultData(a.repeat(63_000))), ToolResult(second_tool, true, StringResultData(b.repeat(63_000))) ) val message ConversationMarkupManager.buildToolResultMessage(results) val resultBlocks ChatMarkupRegex.toolResultAnyPattern.findAll(message).toList() assertEquals(2, resultBlocks.size) assertTrue( resultBlocks.all { it.value.length ToolExecutionLimits.MAX_SINGLE_TOOL_RESULT_MESSAGE_CHARS } ) assertTrue(message.contains(name\first_tool\)) assertTrue(message.contains(name\second_tool\)) }这条测试精准地复现了旧缺陷场景两个工具各产生 63,000 字符的结果旧逻辑下两条合计 126,000 字符远超整轮上限第二个结果必被丢弃新逻辑下断言两个结果块都被保留assertEquals(2, resultBlocks.size)每条独立限长均不超过MAX_SINGLE_TOOL_RESULT_MESSAGE_CHARS两个工具的 name 属性都出现在消息中证明槽位一一对应。在原文档增加回归测试一项完成后该测试即成为防止此问题回潮的行为契约。设计取舍与边界说明为什么不在拼接层统一限制从设计意图看整轮统一上限必然造成后发结果被牺牲而并行调用中结果的价值与执行顺序无关——前面的大结果不应挤掉后面的小结果。把预算按调用而非轮次分配是保证多工具语义完整的最小改动。上下文长度如何控制原文档明确说明整体上下文长度继续由 token 统计和总结流程处理。换言之字符级限额只管单条卫生对话窗口的总长度由 token 计数与上下文总结/裁剪机制在更上层治理两者各司其职。前提与限制本改造针对的是单轮并行多工具的结果保留不改变单个工具自身的输出截断如 DebuggerFileSystemTools.kt 中MAX_FILE_READ_BYTES的文件读取截断、LinuxFileSystemTools.kt 的[File truncated...]提示等也不改变各 Provider 对历史长度自身的限制策略。总结Operit 的工具结果按调用独立限制改造是一次典型的限制粒度重构把长度预算从轮次共享改为单调用独立配合图片链接的独立预算与多结果保留回归测试确保并行多工具调用时每个tool_result都有对应槽位、结构完整、单条不越界。其核心代码集中在 ConversationMarkupManager.kt 与 ToolExecutionLimits.kt验证契约在 ConversationMarkupManagerResultLimitTest.kt。对于同样面临多工具结果互相挤占预算问题的长上下文 Agent 工程这一单条限长 整轮保留 上层 token 治理的分层策略具有直接参考价值。赞分享AI Agent人工智能大模型AI 应用工具调用本地部署MCP ClientsAgent 记忆【免费下载链接】OperitThe most powerful AI agent and AI chat software on Android/Operit是一款Android上能力最为强大、发展最久的AI Agent项目地址https://gitcode.com/gh_mirrors/op/Operit点击查看免费下载相关推荐如何理解fzf的高效结果合并merger.go的核心策略解析如何理解fzf的高效结果合并merger.go的核心策略解析 fzf作为一款强大的命令行模糊查找工具其高效的结果合并机制是实现快速搜索体验的关键。本文将深入CLICrossfilter与D3.js集成指南打造专业级数据可视化应用Crossfilter与D3.js集成指南打造专业级数据可视化应用 Crossfilter是一个强大的JavaScript库专为在浏览器中探索大型多元数据集Markdown编辑器的未来趋势从Awesome Markdown Editors看技术演进Markdown编辑器的未来趋势从Awesome Markdown Editors看技术演进 Markdown作为一种轻量级标记语言已成为内容创作、文档编写上一篇探索极速内存数据库MasterMemory下一篇Qwopus3.5-9B-Coder-GGUF三阶段课程学习策略从基础到高级的渐进式训练创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考