首页
/ screenshot-to-code 后端 Agent 工具调用流程深度解析:从 Provider 流式回合到工具执行与多供应商续聊机制

screenshot-to-code 后端 Agent 工具调用流程深度解析:从 Provider 流式回合到工具执行与多供应商续聊机制

2026-09-04 15:39:30作者:裴麒琰

本文基于仓库设计文档 design-docs/agent-tool-calling-flow.md 展开,梳理 screenshot-to-code 中一个 variant 开始生成后,后端 Agent 的完整工具调用闭环:入口调用链、AgentEngine 的核心循环、工具执行生命周期、create_file 的实时流式预览,以及 OpenAI / Anthropic / Gemini 三家 Provider 各自不同的"续聊"(continuation)实现。读完本文,你将能够理解该项目如何在统一的会话契约下抹平三家模型 API 的差异,并通过 WebSocket 把思考流、工具事件和代码预览实时推送到前端。

入口:从生成阶段到 Agent 引擎

每个 variant 的生成都由一次 Agent(...).run(model, prompt_messages) 调用驱动,调用点位于 backend/routes/generate_code.py 中的 AgenticGenerationStage._run_variant。这里的 Agent 是一个刻意保持极薄的封装,其定义见 backend/agent/runner.py

class Agent(AgentEngine):
    pass

它直接继承 AgentEnginebackend/agent/engine.py),不附加任何逻辑——这意味着所有编排能力都集中在引擎里,入口类仅作为语义化的对外门面。这种"薄 wrapper + 厚引擎"的结构,使得 design-docs/agent-tool-calling-flow.md 所描述的循环行为与实际代码一一对应。

AgentEngine.run 在正式进入循环前做了两件准备工作(见 backend/agent/engine.py):

  1. 通过 _extract_input_imagesprompt_messages 中抽取 data:image/ 前缀的静态图片,注入 tool_runtime.input_images,供 extract_assets 等工具裁剪使用;
  2. 调用 seed_file_state_from_messagesbackend/agent/state.py),从历史 assistant 消息或 system 消息中的 "Here is the code of the app:" 标记后提取既有代码,回填内存文件状态 AgentFileState,保证编辑类请求(update 流程)能拿到原始文件。

随后引擎调用 backend/agent/providers/factory.pycreate_provider_session,按模型所属的供应商族(OPENAI_MODELS / ANTHROPIC_MODELS / GEMINI_MODELS)实例化对应的 ProviderSession,并把规范化后的工具定义序列化后传入。

核心工具调用循环:_run_with_session

主循环位于 backend/agent/engine.pyAgentEngine._run_with_session。结合设计文档,其每一轮迭代的行为可以拆解为五个阶段:

1. 建立轮次本地流式状态。 为 assistant / thinking 两类事件流生成带 variant 序号的 event ID(_next_event_id,格式为 {prefix}-{variant_index}-{uuid8}),并初始化两个去重容器:

  • started_tool_ids: set[str] —— 记录哪些 tool_call_id 已经发过 toolStart,避免流式参数阶段和正式执行阶段重复发送;
  • streamed_lengths: Dict[str, int] —— 记录每个工具调用已通过增量 delta 推送了多少 content 长度,用于控制增量预览的步长。

2. 流式拉取一个 Provider 回合。 引擎调用 turn = await session.stream_turn(on_event)on_event 回调按事件类型分派(见 backend/agent/engine.py):

事件类型 引擎动作 对应 WebSocket 消息
assistant_delta 累积助手文本并即时转发 assistant
thinking_delta 即时转发思考内容 thinking
tool_call_delta 交给 _handle_streamed_tool_delta 处理(见下文"流式预览"一节) toolStart / setCode

3. 按工具调用分支。turn.tool_calls 为空,说明模型已给出最终答案,引擎直接进入 _finalize_response 收尾并返回;否则逐个执行本轮的所有工具调用,其间发出完整的工具生命周期消息(toolStart → 可选的 setCode 预览 → toolResult),并把每个结果打包成 ExecutedToolCall(tool_call, result) 收集起来。

4. 用工具结果续写会话。 调用 session.append_tool_results(turn, executed_tool_calls),把助手回合与工具输出追加进该供应商的历史结构,下一轮循环即带着更新后的历史再次请求模型。

5. 护栏。 设计文档写明"最多 20 轮工具回合";从当前源码看,循环上限为 max_steps = 30backend/agent/engine.py),超出后抛出 Agent exceeded max tool turns 异常——这是防止模型陷入无限工具调用的最后一道保险。此外源码中还有一个文档未展开的护栏:每次工具执行前会检查 session.total_cost_usd(),若已计价模型的花费超过 GENERATION_MAX_COST_USDbackend/config.py),则抛出 BudgetExceededError 终止该 variant。

工具执行:运行时与规范化工具集

工具运行时

实际执行由 backend/agent/tools/runtime.py 中的 AgentToolRuntime.execute(tool_call) 完成。它先检查参数是否解析失败(带 INVALID_JSON 标记的参数会被原样回传给模型,让模型自行修正),然后按 tool_call.name 分派到各实现。工具结果统一封装为 ToolExecutionResult,其字段分工清晰:

  • result:回传给模型的完整结构化结果(OpenAI 侧会 json.dumps 后作为 function_call_output);
  • summary:用于前端 toolResult 消息的摘要;
  • updated_content:文件内容变化(如 create_file / edit_file 后),触发前端 setCode
  • multimodal_parts:多模态附件(如生成的图片),供部分 Provider 在续聊时以图片形式回传给模型。

设计文档列出的基础工具集(create_fileedit_filegenerate_imagesremove_backgroundretrieve_option)在源码的 canonical_tool_definitionsbackend/agent/tools/definitions.py)中是常驻项,而 edit_imageextract_assetsscreenshot_previewsave_assets 则按能力开关条件挂载。工厂层(backend/agent/providers/factory.py)决定开关的取值:

  • image_editing_enabled:需要 Replicate API key;
  • asset_extraction_enabled:需要 Gemini key 且本次请求确实包含可裁剪的静态截图;
  • screenshot_enabled:依赖 is_screenshot_preview_available(),即无头 Chromium 是否可启动。

create_file 为例,其参数 schema 明确要求 content(完整 HTML),path 缺省回退为 index.html;执行时 _create_file 会先经 extract_html_content 剥离 Markdown 代码围栏,再写入 file_state 并返回成功消息加文件元数据。edit_file 则要求 old_text 精确匹配文件内容,支持 edits 批量编辑与 count=-1 全量替换,并生成 unified diff 和首个变更行号写回给模型(backend/agent/tools/runtime.py)。

每个工具调用的执行生命周期

结合引擎源码,每个工具调用的完整生命周期为(backend/agent/engine.py):

  1. 发送 toolStart(若该 tool_call_id 已在流式参数阶段提前发送过则跳过,由 started_tool_ids 保证去重);
  2. 若工具为 create_file,先调用 _stream_code_preview 以分块方式推送预览代码——此阶段属于"纯展示",计时器(AgentRunRecorder)在其后才启动,使工具耗时统计只包含真实执行;
  3. 调用 tool_runtime.execute(tool_call) 真正执行;
  4. 若结果携带 updated_content,发送 setCode 用最终内容覆盖预览;
  5. 发送 toolResult,payload 为 { name, output, ok }

create_file 的流式预览:不等参数解析完成就开始渲染

这是整个流程中最精巧的部分。模型生成 create_file 时,参数 JSON 是逐 token 到达的,整段 content 在回合结束前并不存在完整 JSON。引擎通过 backend/agent/tools/parsing.py 中的两个解析器从不完整的 JSON 文本中提取出部分字段:

  • extract_content_from_args(raw_args):若参数已是 dict 则直接取 content;否则调用 _extract_partial_json_string(raw_text, "content"),定位 "content" 键,扫描字符串值的引号配对(处理转义引号)、剥掉尾部未闭合的转义反斜杠,先尝试 json.loads('"' + partial + '"'),失败则回退到手写替换 \n\t\r\"\\ 五个转义序列——从而在 JSON 尚未闭合时也能还原出已经生成的 HTML 片段;
  • extract_path_from_args(raw_args):同法提取 path

引擎侧的 _handle_streamed_tool_deltabackend/agent/engine.py)对每个 tool_call_delta 事件做两件事:

  • 若该 tool_call_id 尚未 toolStart,则立即用已知的 path(回退到 file_state.pathindex.html)和当前 content 预览(summarize_text(content, 200))发送提前的 toolStart
  • 增量推送 setCode:首次拿到 content 时整段发送,之后每当新增长度 ≥ 40 字符再发一次全量覆盖,避免高频小 delta 冲击 WebSocket。

其效果是:模型仍在"打字"时,前端预览窗已经在逐段渲染 HTML。设计文档对此的表述是"允许前端在实际工具执行完成之前预览"。回合结束后引擎还会用 _stream_code_preview 把最终完整内容按最多 18 块、每块至少 200 字符的节奏补齐推送(backend/agent/engine.py),确保预览状态与真实文件状态一致。

供应商契约与三家 Provider 的续聊差异

统一契约

所有 Provider 会话都实现 backend/agent/providers/base.py 中的 ProviderSession Protocol:

  • stream_turn(on_event) -> ProviderTurn:执行一轮模型请求,把流事件实时交给 on_event,返回一个 ProviderTurn
  • append_tool_results(turn, executed_tool_calls):把本轮助手输出与工具结果并入历史;
  • total_cost_usd() -> Optional[float]:未计价模型返回 None
  • close():释放客户端。

其中 ProviderTurn 的三个字段是续聊的基石:assistant_text(助手文本,用于收尾)、tool_calls(解析后的 ToolCall 列表)、assistant_turn——供应商原生的助手回合对象,引擎不理解也不修改它,只在续聊时原样交还。这一设计把三家 API 的结构差异完全隔离在各自实现内。

OpenAI:Responses API 的 item 追加

backend/agent/providers/openai.pyOpenAIProviderSession.append_tool_results

  1. turn.assistant_turn(即 response.output_item.done 事件收集到的原生 output items,含 function_call 条目)追加进 self._input_items
  2. 为每个工具结果追加一条 {"type": "function_call_output", "call_id": ..., "output": json_string};若结果携带多模态附件且执行成功,output 升级为 [input_text, input_image, ...] 数组,让模型在下一轮同时看到结构化 JSON 和真实图片(本地资产会先 base64 编码为 data URL)。

下一轮 responses.create 直接使用这份更新后的 item 列表。注意工具 schema 在 serialize_openai_tools 中会被 _make_responses_schema_strict 改造为 strict 模式(object 强制 additionalProperties: false、属性类型补 null 并全部标记 required),以适配 Responses API 的 strict 工具约束。

Anthropic:text + tool_use 块与 tool_result 消息

backend/agent/providers/anthropic/provider.pyAnthropicProviderSession.append_tool_results

  1. 追加一条 assistant 消息,内容块依次为可选的 text 块和若干 tool_use 块(携带 idnameinput);
  2. 追加一条 user 消息,内容为一组 tool_result 块,每个块含 tool_use_id、序列化后的结果内容与 is_error 标记。源码注释指出一个实现细节:API 在 is_error 时拒绝非文本的 tool_result 内容,因此错误结果会被降级为纯文本。

下一轮 messages.stream 从这些块继续。

Gemini:原样回传 model parts

backend/agent/providers/gemini.pyGeminiProviderSession.append_tool_results

  1. turn.assistant_turn(即上一轮模型返回的原始 content)原样追加——设计文档特别指出,这是为了保留 model part 结构,包括 thought signature 敏感流程所需的完整性;
  2. 为每个工具追加 role="tool" 的 content,使用 Part.from_function_response 构造。

三家实现的对比可以概括为:OpenAI 追加 item、Anthropic 追加 消息块对、Gemini 追加 content parts——引擎层面只需要一个"黑盒"的 assistant_turn 即可完成续聊,这正是该契约的抽象价值所在。

前端流式消息与收尾

生成期间引擎通过 send_message 向 WebSocket 推送五类消息:assistantthinkingtoolStarttoolResultsetCode。来源分三层:Provider 解析器在 stream_turn 中产出 StreamEvent delta;引擎经 on_event 即时转发;工具执行阶段再补充显式的生命周期事件与代码更新。一个典型回合的消息时序为:

  1. thinking / assistant delta;
  2. tool call delta(可选,驱动 create_file 的提前 toolStart 与增量 setCode);
  3. toolStart
  4. setCode 预览(仅 create_file,可选);
  5. toolResult
  6. 下一轮模型请求开始,循环重复。

收尾逻辑在 _finalize_responsebackend/agent/engine.py):优先返回内存文件状态 file_state.content;若文件状态为空,则尝试用 extract_html_content 从最终助手文本中提取 HTML 作为兜底,并同步一次 setCode。此外 run 方法用 BaseException 捕获所有异常(包括客户端断连导致的取消),确保 AgentRunRecorder 的 run 记录不会永远停留在 "running",finally 中调用 session.close() 释放供应商客户端并打印 token 用量与成本汇总。

模块地图

职责 文件
引擎编排(主循环、流式预览、收尾) backend/agent/engine.py
Agent 入口 backend/agent/runner.py
供应商工厂(按模型实例化会话) backend/agent/providers/factory.py
供应商契约(ProviderSession / ProviderTurn / StreamEvent backend/agent/providers/base.py
OpenAI 实现 backend/agent/providers/openai.py
Anthropic 实现 backend/agent/providers/anthropic/provider.py
Gemini 实现 backend/agent/providers/gemini.py
工具规范化定义 backend/agent/tools/definitions.py
工具执行运行时 backend/agent/tools/runtime.py
部分 JSON 参数解析 backend/agent/tools/parsing.py
工具输入摘要 backend/agent/tools/summaries.py
文件状态与历史回填 backend/agent/state.py

整体来看,screenshot-to-code 的 Agent 后端用一个统一的 ProviderSession 契约把"流式回合 + 原生续聊对象"抽象出来,让引擎专注于工具循环与前端事件编排;工具侧则以"规范化定义 → 供应商序列化 → 运行时执行"三层分离,配合部分 JSON 解析实现 create_file 的即时预览。理解这套流程,是读懂该项目多供应商 Agent 生成、排查 toolResult 事件异常或扩展新工具的基础。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.83 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
506
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384