news 2026/6/10 16:03:11

Microsoft Agent Framework —— CodeAct:Agent写代码,沙箱执行

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Microsoft Agent Framework —— CodeAct:Agent写代码,沙箱执行

概述

本文聚焦 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/inkwell

Microsoft Agent Framework 官方项目地址:https://github.com/microsoft/agent-framework

Microsoft 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生命周期,适合配置稳定的场景。

  • 配置与能力描述层:HyperlightCodeActProviderOptionsCodeActApprovalModeFileMountAllowedDomain。决定后端(SandboxBackend.WasmSandboxBackend.JavaScript,Wasm 下的沙箱端语言取决于加载的模块,示例里用的是官方 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 + 模型"五方协作完成,关键路径如下:

  1. Agent 接到用户消息,遍历AIContextProviders调用ProvideAIContextAsync,CodeActProvider 当场冻结一份RunSnapshot,根据快照算approvalRequired,构造本次运行专用的ExecuteCodeFunction注入到AIContext.Tools

  2. ChatClient 把这份工具列表拼进请求发给模型;模型决定调用execute_code({code: "..."})

  3. 框架判断execute_code是否被ApprovalRequiredAIFunction包裹,是则不直接执行,而是把ToolApprovalRequestContent作为响应返回,等待回路确认。

  4. 审批通过后框架真正调用ExecuteCodeFunction.InvokeAsync,由SandboxExecutor.ExecuteAsync接管。

  5. SandboxExecutor检查当前Sandbox实例的指纹是否与本次快照一致:一致就Restore(warmSnapshot)复用,不一致就重建沙箱、重新注册工具、做一次预热并捕获新的预热快照。

  6. 代码在沙箱内执行;遇到call_tool("name", args)时进入ToolBridge.InvokeAsync,按 JSON 反序列化为AIFunctionArguments,调用宿主AIFunction.InvokeAsync,结果通过AIJsonUtilities序列化回沙箱。

  7. 执行结束后框架把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)。首次构建沙箱之后会做一次与后端相关的空操作(Nonevoid 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 Platform,PowerShell 执行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(详见上一节"模块的扩展性"对应说明)。

  • 资源:HeapSizeStackSize控制沙箱端堆栈上限,越界会让沙箱兜底为{exit_code: -1, success: false}

没有执行时长强制截断

MAF 这层把CancellationToken一直传到SandboxExecutor.ExecuteAsync,但底层sandbox.Run(code)是同步阻塞调用——cancellationToken实际生效在SemaphoreSlim的排队阶段;一旦沙箱真正开始跑,就只能等它结束。生成的代码里不要写死循环或无界流式拉取,没有强制截断的机制。

并发也不是真并行:同一个SandboxExecutorSemaphoreSlim(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 扩展不可加载numpypandas这类带二进制 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 panic):SandboxExecutor兜成{ stdout: "", stderr: ex.Message, exit_code: -1, success: false }

  • call_tool调宿主工具异常:ToolBridge捕获并序列化成{ "error": "<message>" }回沙箱端,由沙箱端代码决定怎么处理。

审批模型:bundled approval 的判定与影响

CodeAct 的审批语义来自HyperlightCodeActProvider.ComputeApprovalRequired,判定条件只有两个:

  • ApprovalMode设为AlwaysRequire

  • 当前RunSnapshot.Tools中任意工具的GetService<ApprovalRequiredAIFunction>()不为 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:何时选哪一种

两种集成路径的差异不在"能不能用",而在"生命周期由谁掌控"和"运行期是否允许配置变动"。

维度

HyperlightCodeActProvider

HyperlightExecuteCodeFunction

注入方式

AIContextProviders,每次运行重新ProvideAIContextAsync

直接作为AIFunction放进tools

配置时机

每次运行现取RunSnapshot,支持运行期增删工具/挂载/白名单

构造时一次性快照,整个 agent 寿命共用

审批发现

直接用ApprovalRequiredAIFunctionexecute_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 二选一;如果未来需要嵌入到DelegatingChatClientFunctionInvokingChatClient,可以参考HyperlightExecuteCodeFunctionGetService模式自行扩展。

一个容易被忽略的扩展点: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 上线拆成三段,每段都对应一个清晰的验证目标:

  1. 纯解释器形态。不挂任何宿主工具,验证"模型愿意稳定生成可执行代码 + 沙箱跑得通"这件事本身。这一段失败大多不是 CodeAct 问题,而是沙箱端模块路径、运行权限、模型系统提示词不到位。

  2. 只读工具桥接。挂 1 到 2 个只读宿主工具,让模型在execute_code里调用call_tool,重点观察:参数类型是否一致、错误分支是否被处理、沙箱端标准输出是否能稳定承载结构化结果。

  3. 审批 + 审计。引入ApprovalRequiredAIFunction触发 bundled approval,验证审批回路(ToolApprovalRequestContentCreateResponse→ 同一 sessionRunAsync)端到端走得通;同时把execute_code维度的stdout/stderr/exit_code接入审计与告警。

三段之间不要急着合并,每段都要在生产链路(含网关、可观测性、回滚预案)跑过再进入下一段。CodeAct 真正容易出问题的不是"写不出代码",而是"审批漏接、错误吞了、stderr没人看"。

系统提示词建议长期保留三条约束:

  • 优先把相关步骤合并在单个execute_code块中,减少模型往返。

  • 每次call_tool必须检查错误分支(结果可能是{ "error": "..." }这种结构),失败时显式短路。

  • 不要假设execute_code调用之间有状态:跨调用要传值的,由生成的代码自己显式带上。

示例

CodeAct 的示例可以按"能力成熟度"理解,而不是按代码文件顺序理解:

  • 解释器形态。验证生成 → 执行 → 回传主链路;典型场景是数值计算、统计分析、临时数据处理。

  • ToolEnabled 形态。验证跨沙箱边界的工具桥接和 bundled approval;典型场景是"查文档 + 查数据 + 总结"的复合任务,能直观看出call_toolApprovalRequiredAIFunction联动的现象级表现。

  • ManualWiring 形态。验证不走 Provider、直接把HyperlightExecuteCodeFunctionAIFunction挂的集成方式;典型场景是已有AIContextProvider集合不希望被打乱、或配置稳定希望最小化运行期开销。

三种形态共同回答的是同一个问题:CodeAct 的核心价值不是"更多工具",而是"执行面收敛后可控性更强"。

总结

CodeAct 在 MAF 中提供了一条同时兼顾效率、隔离和治理的执行路径。它把多步工具编排合并到一次代码执行,把宿主能力以call_tool形式受控桥接进入沙箱,把审批 / 审计 / 可观测性收敛到execute_code这一个入口。Provider 与直连 Tool 两种形态对应不同生命周期偏好,快照与预热 + 指纹复用机制保证了多次执行的低开销。

适用场景:

  • 需要串联检索、查询、聚合、总结等多步流程的复合任务。

  • 对执行隔离、审批和审计有明确要求的企业内部 Agent。

  • 希望在不重写既有工具实现的前提下,把 Agent 从单纯的问答升级到"可执行编排"。

最佳实践策略:

  • 先用纯解释器形态打通主链路,再逐步引入工具与审批,每一步在生产链路独立验证。

  • 对所有写操作或外部副作用工具统一ApprovalRequiredAIFunction,并实现稳定的审批回路。

  • 围绕execute_codestdout/stderr/exit_code建立日志、指标、采样回放,使 CodeAct 维度可观测。

  • 把 CodeAct 视为独立能力面做版本治理(工具集、出站白名单、挂载、审批模式各自演进),避免与业务工具实现、系统提示词层强耦合。

  • 当工具集会跨租户 / 会话变化时优先 Provider 路径;当配置在部署时就已经定死、追求最小运行期开销时优先直连HyperlightExecuteCodeFunction

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/6/7 17:22:02

DeepSeek砍价75%说永久,我看到了三个更深的信号

DeepSeek砍价75%说"永久"&#xff0c;我看到了三个更深的信号降价75%&#xff0c;还说是永久的5月22号晚上&#xff0c;DeepSeek发了个公告&#xff0c;我看完直接愣了一下。不是小打小闹的打折&#xff0c;是永久降价75%。原来2.5折的促销价&#xff0c;到期后不再恢…

作者头像 李华
网站建设 2026/6/10 6:52:13

SecureLearn:面向传统ML模型的攻击无关数据投毒防御框架

1. 项目概述与核心挑战 在机器学习&#xff08;ML&#xff09;项目从实验室走向真实世界应用的过程中&#xff0c;一个长期被低估但极其致命的威胁正浮出水面&#xff1a;数据投毒攻击。想象一下&#xff0c;你花费数月精心收集、标注数据&#xff0c;训练出一个准确率高达95%的…

作者头像 李华
网站建设 2026/6/10 10:42:15

Chiseling方法:高效精准识别治疗优势亚组的统计推断框架

1. 亚组选择&#xff1a;从“一刀切”到“量体裁衣”的必然之路在药物研发和临床实践中&#xff0c;我们长期面临一个核心矛盾&#xff1a;一种新疗法在整体人群的随机对照试验中可能只显示出微弱甚至不显著的疗效&#xff0c;但这背后&#xff0c;是否隐藏着一部分对治疗反应极…

作者头像 李华
网站建设 2026/6/10 6:59:47

联邦学习梯度泄露:四种隐私攻击原理与差分隐私防御实践

1. 项目概述&#xff1a;当梯度成为隐私的“告密者”在分布式机器学习&#xff0c;尤其是联邦学习的实践中&#xff0c;我们常常需要共享模型训练的梯度来协同优化一个全局模型。这听起来很美&#xff1a;数据不出本地&#xff0c;只传梯度&#xff0c;隐私似乎得到了保护。然而…

作者头像 李华
网站建设 2026/5/30 4:11:13

Unity TextMeshPro中文方块问题根因与全链路排查指南

1. 这不是字体问题&#xff0c;是Unity底层文本渲染链路的“断点”你刚把TextMeshPro组件拖进场景&#xff0c;输入一行中文&#xff0c;预览框里赫然跳出一排整齐的方块——不是乱码&#xff0c;不是问号&#xff0c;是标准的、像素级对齐的□□□□。你下意识去检查字体文件&…

作者头像 李华