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

资讯详情

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

Effect AI Chat:非流式响应保留工具审批结果,避免已审批工具在后续轮次重复执行

Effect AI Chat:非流式响应保留工具审批结果,避免已审批工具在后续轮次重复执行 Effect AI Chat非流式响应保留工具审批结果避免已审批工具在后续轮次重复执行【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect本篇文章聚焦 Effect 仓库中effect包 AI 模块effect/unstable/ai的一个具体变更在非流式non-streaming响应中保留已完成的工具审批tool approval结果使Chat会话能够正确记录审批历史避免已获批准的工人在后续对话轮次被重复执行。文章结合 changeset 变更说明、LanguageModel.ts与Chat.ts的实现细节以及Chat.test.ts中的回归测试帮助读者理解工具审批机制tool-approval-request/tool-approval-response在带状态会话中的生命周期并掌握如何在多轮对话中安全使用需要审批的工具。变更背景一次审批还是反复执行在使用大型语言模型LLM构建 Agent 应用时工具调用tool call往往伴随着“人工审批”环节模型提议调用某个工具应用先向用户展示审批请求用户批准后才真正执行。Effect 的 AI 模块为此提供了一整套内容部件content partstool-approval-request审批请求与tool-approval-response审批响应含approved布尔值。在本次变更之前非流式generateText响应路径上存在一个缺陷审批结果没有完整保留在响应内容中导致Chat在维护会话历史时“看不见”已经完成的审批。其后果是——当用户在后续轮次继续对话时Chat把历史再次交给语言模型模型看到的仍是“待审批”的旧工具调用于是已获批准的工人会再次发起审批、再次执行造成工具副作用重复发生。本 changeset.changeset/pre/ai-approved-tool-results.md对应的修复正是Retain completed tool approval results in non-streaming responses so Chat records them and does not replay approved tools on later turns.即在非流式响应中保留已完成的工具审批结果使Chat记录它们从而在后续轮次中不再重放已审批的工具。审批机制的代码基础从请求到响应在深入变更之前先明确相关概念在源码中的落点LanguageModel.ts、Prompt.tstool-approval-request框架生成的审批请求部件存放在 assistant 消息中。它携带approvalId和对应的toolCallId表示“工具调用需要用户批准后才能执行”。tool-approval-response用户或外部流程对审批请求的响应部件存放在 tool 消息中携带approvalId、approved: boolean以及可选的reason。例如Prompt.toolApprovalResponsePart({ approvalId, approved: true })。tool-result审批通过后执行工具得到的实际结果部件。Prompt.ts中为审批响应定义了 SchemaPrompt.ts{ type: Schema.Literal(tool-approval-response), approvalId: Schema.String, approved: Schema.Boolean }这意味着审批响应是可编码、可校验、可持久化的结构化数据能够安全地进入Chat的会话历史。核心实现collectToolApprovals 与已解决审批的判定LanguageModel.ts中的collectToolApprovals函数负责从整个消息历史中收集审批信息LanguageModel.tsconst { approved, denied } collectToolApprovals( providerOptions.prompt.content, { excludeResolved: true } )其工作方式遍历 assistant 消息收集tool-approval-request记录approvalId→toolCallId与tool-call记录id→ 部件。遍历 tool 消息收集tool-approval-response记录approvalId、approved、reason以及tool-result记录已产生结果的id。将审批响应与其对应的请求、工具调用关联成ApprovalResult若某个toolCallId已经存在tool-result则视为“已解决”resolvedif (options?.excludeResolved toolResultIds.has(request.toolCallId)) { continue }这一步是修复的关键前提判断“审批已完成”的依据是工具结果tool-result是否已经存在于历史中。因此只要非流式响应把“预执行审批工具得到的结果”原样保留下来后续轮次的collectToolApprovals就会把这些审批标记为已解决从而跳过它们不再重放。修复落点preResolvedResults 进入响应内容在generateText非流式路径中当存在待处理审批且有 toolkit 时框架会先执行已审批的工具调用executeApprovedToolCalls并为被拒绝的调用生成拒绝结果createDenialResultsconst approvedResults yield* executeApprovedToolCalls(approved, toolkit, concurrency) const deniedResults createDenialResults(denied) preResolvedResults [...approvedResults, ...deniedResults]随后这些结果被追加进 prompt 内容并以tool-result形式送回 providerproviderOptions.prompt Prompt.fromMessages([ ...providerOptions.prompt.content, ...Prompt.fromResponseParts(preResolvedResults).content ])本次变更则体现在最终返回值的拼接上LanguageModel.ts// Retain pre-resolved results so Chat can recognize completed approvals on later turns. return [...preResolvedResults, ...content, ...toolResults] as ArrayResponse.PartTools在此之前preResolvedResults只被拼进发给 provider 的 prompt用于当轮生成却没有拼进最终返回给调用方的内容。而Chat正是把每次generateText的返回内容追加进会话历史的——因此历史上就缺失了这些tool-result。修复后preResolvedResults与正常生成的内容、工具结果一起返回Chat据此在历史中看到“该审批已有结果”下一轮collectToolApprovals便将其视为已解决不再重放。对于流式路径streamText实现同样遵循这一约定并在注释中明确说明LanguageModel.tsPreserve the existing encoded result representation for streaming approvals.Chat 的端到端行为审批 → 执行 → 不重放Chat是有状态会话服务Chat.ts它在Ref中维护Prompt历史每次生成调用都会将响应部件追加回历史。正是因为“历史由生成返回内容累积而成”保留preResolvedResults才能保证审批状态在会话中一致。仓库中的回归测试直接验证了本变更的行为Chat.test.tsit.effect(does not replay a completed approved tool, () { let executions 0 const toolkit Toolkit.make(Tool.make(Counter, { parameters: Schema.Struct({}), success: Schema.Number, needsApproval: true })) return Effect.gen(function*() { const chat yield* Chat.empty const initial yield* chat.generateText({ prompt: Run the counter, toolkit }).pipe( TestUtils.withLanguageModel({ generateText: [{ type: tool-call, id: counter-call, name: Counter, params: {} }] }) ) const approval initial.content.find((part) part.type tool-approval-request) assert.isDefined(approval) // 第二轮用户批准工具 yield* chat.generateText({ toolkit, prompt: [Prompt.toolMessage({ content: [Prompt.toolApprovalResponsePart({ approvalId: approval.approvalId, approved: true })] })] }).pipe( TestUtils.withLanguageModel({ generateText: [{ type: text, text: Complete }] }) ) // 第三轮继续对话不应再次执行 Counter yield* chat.generateText({ prompt: Continue, toolkit }).pipe( TestUtils.withLanguageModel({ generateText: [{ type: text, text: Continued }] }) ) assert.strictEqual(executions, 1) // 工具只执行一次 }).pipe( Effect.provide(toolkit.toLayer({ Counter: () Effect.sync(() executions) })) ) })测试拆解第一轮模型发起Counter工具调用因needsApproval: true响应中出现tool-approval-request部件。第二轮用户通过Prompt.toolApprovalResponsePart({ approvalId, approved: true })批准审批通过后工具在本轮执行executions变为 1执行结果以tool-result形式随非流式响应返回并被Chat记录进历史。第三轮继续普通对话。由于历史中已存在该调用的tool-resultcollectToolApprovalsexcludeResolved: true将其判定为已解决不会再次执行Counter。最终断言executions 1证明已审批工具在后续轮次不会被重放。配套行为清理已解决的审批残留修复并非简单“保留一切”。为了避免向 provider 发送过期的审批部件例如 OpenAI 会拒绝引用其从未发出过的mcp_approval_response条目LanguageModel.ts提供了stripResolvedApprovals函数在真正调用模型前从 prompt 中剥离已解决审批对应的tool-approval-request与tool-approval-responseLanguageModel.tsconst stripResolvedApprovals ( prompt: Prompt.Prompt, approved: ReadonlyArrayApprovalResult, denied: ReadonlyArrayApprovalResult ): Prompt.Prompt { const resolvedApprovalIds new Setstring() // ...收集所有已解决审批的 approvalId // 过滤掉已解决的 tool-approval-request / tool-approval-response 部件 }仓库测试对此有明确断言LanguageModel.test.tsstrictEqual(msg.content.filter((p) p.type tool-approval-request).length, 0) strictEqual(msg.content.filter((p) p.type tool-approval-response).length, 0)可见整体设计是“两手抓”对外部provider剥离已解决的审批部件对内部Chat 历史/响应内容保留审批执行结果。前者保证 provider 兼容性后者保证会话状态正确。边界情况缺少 Toolkit 或工具缺失时的错误路径如果历史中存在待处理审批但当前生成调用没有提供 toolkit框架不会静默忽略而是抛出ToolkitRequiredError明确指出哪些工具正等待审批LanguageModel.tsif (hasPendingApprovals) { return yield* AiError.make({ module: LanguageModel, method: generateText, reason: new AiError.ToolkitRequiredError({ pendingApprovals: [...approved, ...denied] .map((result) result.toolCall?.name) .filter(Predicate.isNotUndefined) }) }) }类似地若审批对应的工具在 toolkit 中不存在会抛出ToolNotFoundError附带可用工具列表LanguageModel.tsnew AiError.ToolNotFoundError({ toolName: approval.toolCall.name, availableTools: Object.keys(toolkit.tools) })这些错误路径保证会话不会在“缺工具”或“工具被移除”的状态下继续盲目推进审批流程。总结与建议本变更的核心价值在于修复了带状态Chat会话中工具审批的一次性语义非流式响应generateText现在会保留预执行审批产生的tool-result结果确保Chat历史完整记录“审批已完成”这一事实后续轮次通过collectToolApprovals(..., { excludeResolved: true })将带有tool-result的审批识别为已解决从而不再重放已审批的工具调用避免副作用重复同时stripResolvedApprovals会在调用 provider 前清理已解决的审批部件维持与各 provider 的兼容性。对于使用 Effect AI 构建 Agent 的开发者实践建议如下多轮对话中的审批工具确保Chat会话历史不丢失任何一轮generateText的返回内容否则审批状态仍可能断裂。审批工具必须常驻 toolkit后续轮次仍需传入包含该工具的 toolkit否则会触发ToolkitRequiredError/ToolNotFoundError。回归验证可参考 Chat.test.ts 中的 “does not replay a completed approved tool” 测试用计数型工具needsApproval: true验证工具是否只执行一次。相关实现与测试文件变更说明.changeset/pre/ai-approved-tool-results.md审批收集、预执行与剥离逻辑LanguageModel.ts审批响应部件与 SchemaPrompt.ts有状态会话实现Chat.ts回归测试Chat.test.ts、LanguageModel.test.ts【免费下载链接】effectBuild production-ready applications in TypeScript项目地址: https://gitcode.com/GitHub_Trending/ef/effect创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表