
概述本文聚焦 MAF 的 CodeAct 能力以 MAF 1.6.1含 Microsoft.Agents.AI.Hyperlight 1.6.1-preview.260514.1为基线说明该模块的职责边界、核心调用链、扩展方式与生产落地策略。文档目标不是复述示例代码而是帮助你理解 CodeAct 作为“可执行编排面”的设计意义和工程取舍。之前写了一篇文章分享了我对Harness Engineering的使用探索Harness Engineering 实战 —— 把 Harness Engineering 做成一个一键安装的项目开源项目https://github.com/shuaihuadu/inkwellMicrosoft Agent Framework 官方项目地址https://github.com/microsoft/agent-frameworkMicrosoft Agent Framework 正在定义下一代 AI 应用的标准——你的技术栈该刷新了我建了个交流群欢迎加入一起学习不掉队。Microsoft Agent Framework交流群CodeAct 是什么CodeAct 是 MAF 把多步工具编排从模型多轮往返压成一次代码执行的范式。模型不再被诱导成一步一个 function call而是把要做的事写成一段可执行代码由受控沙箱代为运行把结构化结果回传给模型再做表达层处理。它真正解决的问题不是让模型会写代码而是三件长期困扰 Agent 工程化落地的事工具往返成本。传统函数调用function-calling的代价不是一次工具调用而是工具调用 × 模型轮次的叉乘。CodeAct 把整段编排合并成一次执行模型上下文里只新增一段标准输出stdout不会被多轮 tool/role 信息撑爆。执行边界模糊。模型生成的代码如果直接跑在宿主进程相当于把宿主能力面整体暴露给一段不可信文本。CodeAct 把执行下沉到 Hyperlight 微型虚拟机宿主能力只通过显式注册的call_tool桥接进入能力面是白名单 入口集中。工具治理碎片化。直接把十几个AIFunction挂到模型上会出现接口描述体积膨胀、模型挑错工具、审批界面被拆碎等一连串次生问题。CodeAct 把模型可见面收敛到一个execute_code治理点天然集中。一句话总结CodeAct 不是多一个工具而是把工具的使用面从模型外移到了沙箱内。模块组成与职责拆分CodeAct 在 MAF 中由六个职责分明的内部组件协作完成可以按从上到下的层次理解上下文注入层HyperlightCodeActProviderAIContextProvider。负责在每次运行前向 agent 注入execute_code工具与一段 CodeAct 指令是 Provider 路径的入口。直连工具层HyperlightExecuteCodeFunctionAIFunction。把execute_code直接作为AIFunction暴露跳过AIContextProvider生命周期适合配置稳定的场景。配置与能力描述层HyperlightCodeActProviderOptions、CodeActApprovalMode、FileMount、AllowedDomain。决定后端SandboxBackend.Wasm或SandboxBackend.JavaScriptWasm 下的沙箱端语言取决于加载的模块示例里用的是官方 Python 模块、沙箱端模块、堆栈大小、工具集、挂载、出站白名单、审批模式。运行快照层SandboxExecutor.RunSnapshot。每次运行把当时的工具、挂载、允许名单、输入目录冻结成一份不可变快照避免运行中增删改动作污染当次执行。沙箱生命周期层SandboxExecutor。持有底层Sandbox的构建、预热、快照与恢复、按指纹重建等核心机制。跨沙箱边界桥接层ToolBridge。把AIFunction注册到沙箱端并在沙箱端与宿主之间建立 请求 JSON 入 / 结果 JSON 出 的稳定契约。这六层中开发者直接面对的只有前三层后三层是 CodeAct 真正贵的地方也是文档需要讲清楚的地方。模型可见面与沙箱可见面CodeAct 故意制造了两套工具可见面它们的边界对工程落地非常重要模型可见面只看到一个工具execute_code。该工具的描述由InstructionBuilder.BuildExecuteCodeDescription动态拼出列出当前可用call_tool名字、文件挂载提示、出站白名单等。沙箱可见面沙箱端代码可以通过call_tool(name, ...)调用宿主AIFunction但这些工具不在模型的工具列表里也不会出现在tools接口描述中。这种切分带来三个直接好处模型看到的工具接口描述体积只跟是否启用 CodeAct有关不随宿主工具数量线性膨胀。宿主工具的命名空间不会污染模型的函数调用决策。审批、配额、日志等横切策略可以只盯一个入口execute_code不需要为每个宿主工具重写一遍。这也是为什么文档默认不建议同时把同一批工具既挂到tools又挂到 CodeAct——HyperlightExecuteCodeFunction.BuildInstructions(toolsVisibleToModel: true)提供的简化指令就是给真要这么做的人留的口子但绝大多数业务不需要。调用链与执行时序一次完整的 CodeAct 执行由agent 框架 provider executor sandbox 模型五方协作完成关键路径如下Agent 接到用户消息遍历AIContextProviders调用ProvideAIContextAsyncCodeActProvider 当场冻结一份RunSnapshot根据快照算approvalRequired构造本次运行专用的ExecuteCodeFunction注入到AIContext.Tools。ChatClient 把这份工具列表拼进请求发给模型模型决定调用execute_code({code: ...})。框架判断execute_code是否被ApprovalRequiredAIFunction包裹是则不直接执行而是把ToolApprovalRequestContent作为响应返回等待回路确认。审批通过后框架真正调用ExecuteCodeFunction.InvokeAsync由SandboxExecutor.ExecuteAsync接管。SandboxExecutor检查当前Sandbox实例的指纹是否与本次快照一致一致就Restore(warmSnapshot)复用不一致就重建沙箱、重新注册工具、做一次预热并捕获新的预热快照。代码在沙箱内执行遇到call_tool(name, args)时进入ToolBridge.InvokeAsync按 JSON 反序列化为AIFunctionArguments调用宿主AIFunction.InvokeAsync结果通过AIJsonUtilities序列化回沙箱。执行结束后框架把execute_code返回的结构化 JSON 当作工具结果回灌给模型模型据此生成自然语言回答。这条链路里有四个一旦理解就能避免大量坑的细节RunSnapshot一旦生成本次运行内的所有AddTools/RemoveTools/AddFileMounts都不会影响这次执行——这是线程安全的设计选择也是为什么并发场景下增删改操作不会跑一半被改半。ApprovalRequired在每次运行都重新计算因此你可以通过中途插拔ApprovalRequiredAIFunction来在不重启 agent 的情况下切换审批策略。沙箱不是每次运行都创建。多次指纹相同的运行复用同一个Sandbox只是每次Restore到预热快照拿干净状态。这是 CodeAct 性能可用的真正原因。同一个SandboxExecutor内部用SemaphoreSlim(1, 1)串行执行。同一个 CodeAct provider 上的多个并发execute_code调用会单飞排队不会真并行执行需要高吞吐时需要在上层按会话 / 租户维度拆出多个 provider 实例。call_tool调用宿主工具时任何异常都会被ToolBridge捕获并序列化成{ error: message }回给沙箱端而不是把异常穿透到沙箱外部。沙箱端代码必须把call_tool的返回值当成可能成功也可能是错误对象处理。execute_code的返回契约execute_code永远返回一个结构化 JSON 字符串字段固定为四个stdout沙箱端标准输出包括print(...)内容、被 print 出来的call_tool结果。stderr沙箱端标准错误或框架兜底的异常信息。exit_code沙箱端进程退出码正常情况0框架兜底失败时为-1。success当且仅当exit_code 0时为true。这个契约的工程意义在于你可以围绕execute_code建一层稳定的可观测性而无需关心沙箱端用的是 Python 还是 JavaScript、模型这次生成了什么代码。把这四个字段直接接到日志 / 链路追踪 / 指标上你就拿到了与模型行为无关的执行面服务级别指标SLI。快照与预热性能可用的关键CodeAct 的性能不是沙箱启得快而是沙箱不必每次都重新启。SandboxExecutor用三件事撑住这一点配置指纹ConfigFingerprint。把工具名集合、挂载、出站白名单、输入目录拼成稳定字符串作为沙箱复用键。任何一项变了下一次运行就会重建沙箱都没变就直接复用。预热快照Warm Snapshot。首次构建沙箱之后会做一次与后端相关的空操作None或void 0;来强制沙箱运行时完成延迟初始化然后捕获一份干净快照留作复用。运行前恢复Restore-before-Run。后续每次运行都先Restore(warmSnapshot)回到这份干净状态再执行确保沙箱端没有跨运行的隐式状态泄漏。这就是为什么文档前面强调execute_code 调用之间默认不持久化状态——这不是建议是机制层面的保证。如果你想跨调用共享数据必须显式让模型把上次结果传进新一次execute_code的源码里。Hyperlight 沙箱执行的能力面与限制CodeAct 的执行底座是 hyperlight-dev/hyperlight-wasm按官方 README 的原文定位是a component that enables Wasm Modules to be run inside lightweight Virtual Machine backed Sandbox … to enable applications to safely run untrusted or third party Wasm code within a VM with very low latency/overhead构建在更底层的 hyperlight 项目之上。注意上游 README 同时声明This is experimental code. It is not considered production-grade by its developers, neither is it supported software.生产投放需要自己承担风险与版本固定策略。运行时硬性前置条件直接来自官方 README没有这些条件 Hyperlight 起不来Windows需要启用 Windows Hypervisor PlatformPowerShell 执行Enable-WindowsOptionalFeature -Online -FeatureName HyperVisorPlatform。Linux需要 KVM 或/dev/mshv运行前可用kvm-ok验证。容器/虚拟机内运行需要宿主开启嵌套虚拟化。默认能力面全部需要显式开通Hyperlight 沙箱默认是零能力面不能访问宿主进程、不能联网、不能读文件系统。所有能力都必须显式声明。MAF 这层把声明面收敛在HyperlightCodeActProviderOptions宿主能力只能通过Options.Tools注册的AIFunction由沙箱端用call_tool(name, ...)调用。没有 syscall、没有 P/Invoke、没有访问宿主进程内存的通道。网络默认完全禁止要联网必须Options.AllowedDomains.Add(new AllowedDomain(api.example.com, ...))按 target 可选 method 精确匹配不在名单里的目标直接拒绝。文件系统默认看不到任何宿主文件HostInputDirectory是当前唯一真实挂载的路径以/input暴露给沙箱端FileMount当前主要参与ConfigFingerprint与触发WithTempOutput()没有按条映射 API详见上一节模块的扩展性对应说明。资源HeapSize与StackSize控制沙箱端堆栈上限越界会让沙箱兜底为{exit_code: -1, success: false}。没有执行时长强制截断MAF 这层把CancellationToken一直传到SandboxExecutor.ExecuteAsync但底层sandbox.Run(code)是同步阻塞调用——cancellationToken实际生效在SemaphoreSlim的排队阶段一旦沙箱真正开始跑就只能等它结束。生成的代码里不要写死循环或无界流式拉取没有强制截断的机制。并发也不是真并行同一个SandboxExecutor用SemaphoreSlim(1, 1)串行执行已在调用链与执行时序节展开。Python 沙箱端的语言天花板如果用 Wasm 后端 官方 Python guest也是 MAF 示例的默认选择实际可写的代码受 Python guest 模块本身限制标准库只覆盖 CPython 的一个子集具体覆盖范围由 guest 模块发布版本决定常见的math/statistics/json/re/datetime/ 字符串处理通常可用。**没有pip install**guest 是预编译模块运行期不能装第三方包。不能起进程没有subprocess/os.system/fork/exec——沙箱内根本没有宿主进程概念。原生 C 扩展不可加载numpy、pandas这类带二进制 wheel 的库默认不可用除非换一个把它们静态编译进去的自定义 guest 模块需要自己用 hyperlight-dev/hyperlight-sandbox 构建。多线程 /asyncio取决于 guest 实现不要假设可用模型用单线程同步代码最稳。网络相关库socket/urllib/requests等默认不可用要联网走AllowedDomains白名单且取决于 guest 是否暴露对应能力。JavaScript 后端走的是另一条 guest默认构造即SandboxBackend.JavaScript其语言能力面由该 guest 模块决定约束维度类似但具体可用 API 不同。错误兜底沙箱端代码异常traceback 进stderrexit_code ! 0success false不向宿主抛异常。沙箱本身崩溃OOM、爆栈、guest panicSandboxExecutor兜成{ stdout: , stderr: ex.Message, exit_code: -1, success: false }。call_tool调宿主工具异常ToolBridge捕获并序列化成{ error: message }回沙箱端由沙箱端代码决定怎么处理。审批模型bundled approval 的判定与影响CodeAct 的审批语义来自HyperlightCodeActProvider.ComputeApprovalRequired判定条件只有两个ApprovalMode设为AlwaysRequire当前RunSnapshot.Tools中任意工具的GetServiceApprovalRequiredAIFunction()不为 null即被ApprovalRequiredAIFunction包装过。任一成立本次运行暴露给模型的execute_code就会再被ApprovalRequiredAIFunction包一层。这意味着审批粒度是execute_code整次调用不是某个call_tool子调用。这就是 bundled approval。哪怕模型这次生成的代码完全没调审批必需的工具也会触发审批。它的语义是代码块本身需要被批准而不是代码块里某条特定调用需要被批准。审批次数等于模型把任务拆成几个execute_code块不是固定 1 次也不是按call_tool数算。对HyperlightExecuteCodeFunction直连 Tool 路径来说判定同样成立只是它通过GetService(typeof(ApprovalRequiredAIFunction))把自身延迟包装成审批代理暴露给框架机制等价。工程上必须配合的一件事是客户端要有一段稳定的审批回路从响应中筛ToolApprovalRequestContent调CreateResponse(approved)包成ChatMessage通过同一个AgentSession回传RunAsync。漏掉这段回路最直观的现象是RunAsync跑完但响应里没有文本——因为框架返回的是审批请求本身不是终答。Provider 与直连 Tool何时选哪一种两种集成路径的差异不在能不能用而在生命周期由谁掌控和运行期是否允许配置变动。维度HyperlightCodeActProviderHyperlightExecuteCodeFunction注入方式进AIContextProviders每次运行重新ProvideAIContextAsync直接作为AIFunction放进tools配置时机每次运行现取RunSnapshot支持运行期增删工具/挂载/白名单构造时一次性快照整个 agent 寿命共用审批发现直接用ApprovalRequiredAIFunction包execute_code通过GetService(ApprovalRequiredAIFunction)返回延迟构造的审批代理多 provider 组合单 agent 只能挂一个StateKey固定为HyperlightCodeActProvider重复会被状态键校验拒绝不占StateKey可与其它AIContextProvider自由共存适用场景工具集会动态变、需要按租户/会话切策略配置在部署时已经定死、追求最小运行期开销一个常见判断如果你的工具集合 出站白名单 挂载一旦上线就基本不变用直连 Tool如果你需要按会话/租户/权限动态调整 CodeAct 的能力面用 Provider。模块的扩展性CodeAct 的可扩展面分四层对应到不同的源码入口工具扩展层通过AIFunctionFactory.Create(...)把任意 C# 委托包成AIFunction挂到Options.Tools或运行期AddTools(...)。工具的Name即沙箱端call_tool的第一个参数Description直接进入execute_code的描述让模型读到。执行策略层通过ApprovalMode和按工具包ApprovalRequiredAIFunction控制审批面通过AllowedDomains控制出站通过HeapSize/StackSize控制资源通过后端切换决定沙箱端语言。数据输入层HostInputDirectory真实挂载到沙箱端/inputFileMount参与ConfigFingerprint计算且会触发WithTempOutput()但当前版本没有按条映射的 API——运行期不会按每条FileMount实际映射路径主要作用是让模型从execute_code的描述里看到这些路径存在等 SDK 暴露更完整挂载 API 后会落到真实运行时能力上。集成形态层Provider 与直连 Tool 二选一如果未来需要嵌入到DelegatingChatClient或FunctionInvokingChatClient可以参考HyperlightExecuteCodeFunction的GetService模式自行扩展。一个容易被忽略的扩展点InstructionBuilder.BuildContextInstructions(toolsVisibleToModel)决定模型读到的 CodeAct 指令措辞。如果你自定义系统提示词又想保留 CodeAct 的语义提示建议直接在自己的指令里拼接这段而不是覆盖掉它。运行准备NuGet 包与 guest 模块CodeAct 落地只需要两个 NuGet 包Microsoft.Agents.AI.Hyperlight是 MAF 这层的封装提供HyperlightCodeActProvider/HyperlightExecuteCodeFunction等类型Hyperlight.HyperlightSandbox.Guest.Python是官方预编译的 Python guest包内带runtimes/{win-x64,linux-x64}/native/python-sandbox.aot。实际运行 Hyperlight 时你需要把这份.aot文件的绝对路径传给HyperlightCodeActProviderOptions.CreateForWasm(...)——MAF 官方示例统一通过HYPERLIGHT_PYTHON_GUEST_PATH环境变量读取这是示例的约定不是 SDK 强制名。这个包默认不会把.aot自动拷贝到bin/所以路径要么指到 NuGet 缓存里的原始位置dotnet nuget locals global-packages --list可查实际路径要么在 csproj 里自己加None Include...拷贝到输出目录。宿主机层面还需要满足 Hyperlight 的硬件虚拟化前置条件Windows 启用 WHP、Linux 准备好 KVM 或/dev/mshv详见上一节Hyperlight 沙箱执行的能力面与限制。使用方式与落地策略建议把 CodeAct 上线拆成三段每段都对应一个清晰的验证目标纯解释器形态。不挂任何宿主工具验证模型愿意稳定生成可执行代码 沙箱跑得通这件事本身。这一段失败大多不是 CodeAct 问题而是沙箱端模块路径、运行权限、模型系统提示词不到位。只读工具桥接。挂 1 到 2 个只读宿主工具让模型在execute_code里调用call_tool重点观察参数类型是否一致、错误分支是否被处理、沙箱端标准输出是否能稳定承载结构化结果。审批 审计。引入ApprovalRequiredAIFunction触发 bundled approval验证审批回路ToolApprovalRequestContent→CreateResponse→ 同一 sessionRunAsync端到端走得通同时把execute_code维度的stdout/stderr/exit_code接入审计与告警。三段之间不要急着合并每段都要在生产链路含网关、可观测性、回滚预案跑过再进入下一段。CodeAct 真正容易出问题的不是写不出代码而是审批漏接、错误吞了、stderr没人看。系统提示词建议长期保留三条约束优先把相关步骤合并在单个execute_code块中减少模型往返。每次call_tool必须检查错误分支结果可能是{ error: ... }这种结构失败时显式短路。不要假设execute_code调用之间有状态跨调用要传值的由生成的代码自己显式带上。示例CodeAct 的示例可以按能力成熟度理解而不是按代码文件顺序理解解释器形态。验证生成 → 执行 → 回传主链路典型场景是数值计算、统计分析、临时数据处理。ToolEnabled 形态。验证跨沙箱边界的工具桥接和 bundled approval典型场景是查文档 查数据 总结的复合任务能直观看出call_tool与ApprovalRequiredAIFunction联动的现象级表现。ManualWiring 形态。验证不走 Provider、直接把HyperlightExecuteCodeFunction当AIFunction挂的集成方式典型场景是已有AIContextProvider集合不希望被打乱、或配置稳定希望最小化运行期开销。三种形态共同回答的是同一个问题CodeAct 的核心价值不是更多工具而是执行面收敛后可控性更强。总结CodeAct 在 MAF 中提供了一条同时兼顾效率、隔离和治理的执行路径。它把多步工具编排合并到一次代码执行把宿主能力以call_tool形式受控桥接进入沙箱把审批 / 审计 / 可观测性收敛到execute_code这一个入口。Provider 与直连 Tool 两种形态对应不同生命周期偏好快照与预热 指纹复用机制保证了多次执行的低开销。适用场景需要串联检索、查询、聚合、总结等多步流程的复合任务。对执行隔离、审批和审计有明确要求的企业内部 Agent。希望在不重写既有工具实现的前提下把 Agent 从单纯的问答升级到可执行编排。最佳实践策略先用纯解释器形态打通主链路再逐步引入工具与审批每一步在生产链路独立验证。对所有写操作或外部副作用工具统一ApprovalRequiredAIFunction并实现稳定的审批回路。围绕execute_code的stdout/stderr/exit_code建立日志、指标、采样回放使 CodeAct 维度可观测。把 CodeAct 视为独立能力面做版本治理工具集、出站白名单、挂载、审批模式各自演进避免与业务工具实现、系统提示词层强耦合。当工具集会跨租户 / 会话变化时优先 Provider 路径当配置在部署时就已经定死、追求最小运行期开销时优先直连HyperlightExecuteCodeFunction。