2026-UE5-AI-开发进展及框架分析
目前 AI 相关功能仍未发布,这里拉取的 ue5-main 分支
笔者版本:e9c4b5cecf4365053d6a8081c5380dccbf1bbe7e [e9c4b5c]
提交日期: 2026年3月27日 19:24:13
UE 的 RoadMap 只提到了 AI Assistant,其他的开发计划都没有列出来,但是很多内容近期都已经开始了,为了避免重复造轮子,以及参考下官方的实现思路,减轻开发成本,考虑记录本篇内容,用于跟踪最新进展,确认 UE 开发计划、框架逻辑,文中不会过多跟踪细节。
主体设计
UE 建了很多插件,但大部分都还是空的,目前比较核心的有如下几个:
AIAssistant,ECABridge,ToolsetRegistry
- AI Assistant,UE 嵌入编辑器的一个 AI 助手,用来调用 UE 自己的 LLM 服务
- ToolsetRegistry,核心的工具集,所有的工具都会注册到这里,可以给 LLM 提供工具列表,也会转发所有的调用到具体工具。同时 FBlueprintLibraryToolset 也提供了 UFunction 的自动收集调用能力。
- ECABridge,MCP 的核心交互类,MCP Server 提供给外部 LLM 服务,手动实现了大量的 Command(目前有 250 文件)
ToolsetRegistry
通过架构图不难看出,ToolsetRegistry 是一个比较核心的模块,来负责注册、协调转发工具逻辑。
UToolsetRegistrySubsystem
ToolsetRegistry 模块内定义了一个 Editor 的 Subsystem,其持有着 ToolsetRegistry,也就是所有的工具集:
UCLASS(BlueprintType, MinimalAPI)
class UToolsetRegistrySubsystem : public UEditorSubsystem
{
GENERATED_BODY()
...
// The set of registered toolset handlers.
UE::ToolsetRegistry::FToolsetRegistry ToolsetRegistry;
};
FToolsetRegistry
FToolsetRegistry 中有两个 TMap,一个是核心的工具集,另一个是自定义 Json 与 UPROPERTY 属性转换的转换器,提供的各个方法也都是这两个 Map 元素的注册、反注册、执行,还有一个比较关键的 GetToolsetJsonSchemas 方法,来提供工具列表的获取:
namespace UE::ToolsetRegistry
{
...
class FToolsetRegistry {
...
UE_API FString GetToolsetJsonSchemas() const;
...
// The set of registered toolset handlers.
TMap<FString, TSharedPtr<FToolset>> ToolsetHandlers;
// The set of registered converters.
TMap<FString, TSharedPtr<FToolsetJsonConverter>> JsonConverters;
// Callbacks to be called when the toolset registry changes.
FOnToolsetRegistryChanged OnToolsetRegistryChanged;
};
}
FToolset
第一个 Map 的 Value FToolset 是工具集的基类,其预留了四个纯虚方法,类很单纯,ExecuteTool 也只是单纯的转发过来,处理逻辑各个 Set 自己按需完成即可。
实现子类注册后会在 FToolsetRegistry 中转调这些方法,这里会以 GetToolsetName 的结果作为键保存到 ToolsetHandlers:
namespace UE::ToolsetRegistry
{
/// Base class for toolsets.
class FToolset
{
public:
...
virtual TFuture<TValueOrError<FString, FString>> ExecuteTool(const FString& ToolName, const FString& JsonInput) = 0;
virtual FString GetToolsetJsonSchema() const = 0;
virtual FString GetToolsetName() const = 0;
virtual FString GetToolsetVersion() const = 0;
virtual UClass* GetToolsetClass() const { return nullptr; }
};
}
最终工具执行时会以 ToolsetName.ToolName 的形式调用,所以会在 ToolsetHandlers 先找到匹配的 Toolset 然后分发调用到各个 ExecuteTool 方法执行。
因为接口也很朴素,可以按实现自行完成,所以目前比较特殊的逻辑,反而都在几个子 ToolSet 里了:
FBlueprintLibraryToolset
这个是比较关键的一个 Toolset,支持传入一个 UClass 来自动生成这一 Toolset:
namespace UE::ToolsetRegistry
{
class FBlueprintLibraryToolset : public FToolset
{
public:
UE_API FBlueprintLibraryToolset(UClass* LibraryClass);
...
// Map of valid tool names to tool call objects.
TMap<FString, TSharedPtr<FObjectFunctionToolCall>> Tools;
// Generate schema from reflection data.
TSharedPtr<FJsonObject> GenerateSchema() const;
// Generate Tools list.
void GenerateToolCallObjects();
// Helper method to generate list of valid tools.
TArray<TObjectPtr<UFunction>> GetValidToolMethodsList() const;
};
}
构造时,其会调用 GenerateToolCallObjects 方法, 然后调用 GetValidToolMethodsList 收集可用的方法,最终填充到 Tools 中:
void FBlueprintLibraryToolset::GenerateToolCallObjects()
{
...
TArray<TObjectPtr<UFunction>> ValidTools = GetValidToolMethodsList();
for (UFunction* Function : ValidTools)
{
check(Function);
Tools.Add(
Function->GetName(),
FObjectFunctionToolCall::Create(
TNotNull<UObject*>(Toolset.Get()), TNotNull<UFunction*>(Function)));
}
}
GetValidToolMethodsList 方法中,会迭代传入 UClass 的 UFunction,将所有公开、静态的方法收集(额外过滤了含有 BlueprintInternalUseOnly meta 的):
TArray<TObjectPtr<UFunction>> FBlueprintLibraryToolset::GetValidToolMethodsList() const
{
...
for (TFieldIterator<UFunction> FuncIt(Toolset.Get(), EFieldIterationFlags::None); FuncIt; ++FuncIt)
{
TObjectPtr<UFunction> Function = *FuncIt;
if (Function)
{
bool bValidFunction = true;
// Skip functions marked as for internal use only.
if (Function->HasMetaData(TEXT("BlueprintInternalUseOnly")))
{
bValidFunction = false;
}
// Skip functions unless they are public and static.
if (!Function->HasAllFunctionFlags(FUNC_Public | FUNC_Static))
{
bValidFunction = false;
}
if (bValidFunction)
{
ValidTools.Add(Function);
}
}
}
return ValidTools;
}
GetToolsetJsonSchema 时会转调 GenerateSchema 方法,其同样会调用 GetValidToolMethodsList 方法,然后迭代 UFunction,并将其转为 FJsonObject,最终再对 Function 名称修正,并补充一些额外信息完成 Schema 生成:
TSharedPtr<FJsonObject> FBlueprintLibraryToolset::GenerateSchema() const
{
...
TArray<TSharedPtr<FJsonValue>> ToolsArray;
TArray<TObjectPtr<UFunction>> ValidTools = GetValidToolMethodsList();
for (UFunction* Function : ValidTools)
{
check(Function);
TSharedPtr<FJsonObject> ToolEntry =
UE::ToolsetRegistry::Internal::ToolsetJson::StructToJsonSchema(Function);
...
ToolsArray.Add(MakeShared<FJsonValueObject>(ToolEntry));
}
Schema->SetArrayField(FString(TEXT("tools")), ToolsArray);
return Schema;
}
这里注意在 StructToJsonSchema 中,虽然以 Struct 传入,但是内部流程中 UFunction 会被特殊处理,以 JSON-RPC 的格式输出:
bool VisitUStruct(const FValidConstUStruct Struct, const TSharedRef<FJsonObject>& OutputSchema,
const FJsonSchemaMemberPath& MemberPath, const FJsonSchemaPropertyFilter& PropertyFilter,
const EVisitorStackElementFlags Flags, const void* InstanceMemory)
{
EditorMetadata.CurrentPropertyMemberPath = MemberPath;
const bool bUseJsonRpcFormat = Struct->IsA(UFunction::StaticClass());
...
if (bUseJsonRpcFormat)
{
// If struct is a function, output JSON RPC -
// https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-06-18/schema.ts#L928
// Extension for 'name' -
// https://github.com/modelcontextprotocol/modelcontextprotocol/blob/main/schema/2025-06-18/schema.ts#L312
// NOTE - We know we are a UFunction, so will have a valid Outer.
OutputSchema->SetStringField(TEXT("name"), *(Struct->GetOuter()->GetName() + "." + Struct->GetAuthoredName()));
...
}
}
最终执行时,会从 Tools 中找到对应的 FObjectFunctionToolCall 对象,然后调用其 Execute 方法,在这个方法中,会对参数校验,并填充默认参数。随后分配了一块 UFunction PropertiesSize 大小的内存 FrameMemory 并初始化,再通过 ToolsetJson::JsonDataToStruct 将来自大模型的参数写入,并补充世界上下文(获得第一个 PIE 或 Game 的 World),最终调用 Function->Invoke 完成函数调用:
TFuture<FJsonValueOrError> FObjectFunctionToolCall::Execute(
const TOptional<FFunctionInputParamsJson>& FunctionInputParamsJson,
TSharedPtr<FToolCallScriptExceptionHandler> ExceptionHandler) const
{
...
// Get function input params JSON as a JSON object.
TSharedPtr<FJsonObject> FunctionInputParamsJsonObject;
FJsonValueOrError JsonValueOrError =
BuildValidFunctionInputParamsJsonObject(FunctionInputParamsJson);
if (JsonValueOrError.HasError())
{
return FJsonValueOrErrorFuture::Make(MoveTemp(JsonValueOrError));
}
...
// Allocate the frame memory.
uint8* FrameMemory;
{
// Invoke must use properties size, because it's allocating locals,
// i.e. for script functions.
const int32 FrameSize = Function->PropertiesSize;
FrameMemory = static_cast<uint8*>(FMemory_Alloca_Aligned(FrameSize,
Function->GetMinAlignment()));
FMemory::Memzero(FrameMemory, FrameSize);
}
...
// We need to manually allocate each property in the frame memory.
for (TFieldIterator<FProperty> It(Function); It && It->HasAnyPropertyFlags(CPF_Parm); ++It)
{
if (const FProperty* ParameterProp = *It;
!ParameterProp->HasAnyPropertyFlags(CPF_ZeroConstructor))
{
ParameterProp->InitializeValue_InContainer(FrameMemory);
}
}
...
if (FrameMemory &&
!ToolsetJson::JsonDataToStruct(
FunctionInputParamsJsonObject.ToSharedRef(), Function, FrameMemory))
{
...
}
...
MaybeSetWorldContextPropertyInFrameMemory(Function, FrameMemory);
...
Function->Invoke(InstanceObject, StackFrame, ReturnValueAddress);
FProperty* ReturnProperty = Function->GetReturnProperty();
...
TSharedPtr<FJsonValue> ReturnJsonValue =
ReturnProperty
? ToolsetJson::PropertyToJsonData(ReturnProperty, ReturnValueAddress)
: MakeShared<FJsonValueNull>();
return FJsonValueOrErrorFuture::Make(MakeResult(MakeValue(ReturnJsonValue)));
}
函数执行后,将返回值转为 Json 返回,完成调用(这里还有一个特殊的 UToolCallAsyncResult 返回类型,会等待异步操作完成后处理 TODO)。
现有的注册都是继承 UToolsetDefinition 的实现,例如 UAgentSkillToolset、UGameplayCueToolset 等,但是从上面的分析中可以看到,通篇并没有利用到 UToolsetDefinition 的任何能力,只要是 UClass 即可,所以实际上引擎内现有的类也都可以作为 FBlueprintLibraryToolset 注册到 FToolsetRegistry 中以供大模型调用。
FECABridgeToolset
所有手动定义的工具,都在这个 Toolset 里,其相关实现,均会通过 FECACommandRegistry 这个单例来进行调用。
FECACommandRegistry 单例中存储有通过 REGISTER_ECA_COMMAND 宏注册的 IECACommand,并会将最终调用转发到每一个 IECACommand 中。
现有的 Command 已经有很多个(目前有 250 个文件),例如:
REGISTER_ECA_COMMAND(FECACommand_GetActorsInLevel)
REGISTER_ECA_COMMAND(FECACommand_CreateActor)
REGISTER_ECA_COMMAND(FECACommand_DeleteActor)
REGISTER_ECA_COMMAND(FECACommand_SetActorTransform)
REGISTER_ECA_COMMAND(FECACommand_GetActorProperties)
REGISTER_ECA_COMMAND(FECACommand_SetActorProperty)
REGISTER_ECA_COMMAND(FECACommand_FindActors)
REGISTER_ECA_COMMAND(FECACommand_DuplicateActor)
大多是偏编辑器侧的操作类工具。
FMCPClientToolset
这个 Toolset 主要是为了获取外部工具,作为 MCP Client 存在,将外部的 MCP Server 的工具注册到 ToolsetRegistry,以便 AI Assistant 可以调用,具体的逻辑不再深入。
TODO
FToolsetJsonConverter
第二个 Map 主要是提供针对特殊属性的 Json 的转换器,对于注册过的属性,在 PropertyToJsonData 时会使用该转换器处理相关逻辑,基类提供的虚方法如下,很简单看名称理解即可:
namespace UE::ToolsetRegistry
{
class FToolsetJsonConverter
{
...
virtual bool CanConvertProperty(TNotNull<const FProperty*> Property) = 0;
virtual TSharedPtr<FJsonObject> PropertyToJsonSchema(TNotNull<const FProperty*> Property) = 0;
virtual TSharedPtr<FJsonValue> PropertyToDefault(TNotNull<const FProperty*> Property, const FString& DefaultString) = 0;
virtual TSharedPtr<FJsonValue> PrTNotNull<FProperty*> Property, const void* Value) = 0;
virtual bool JsonDataToProperty(const TSharedPtr<FJsonValue>& JsonValue, TNotNull<FProperty*> Property,void* OutValue) = 0;
...
};
}
这里提供这一个 Converter,笔者理解是期望能够提供 AI 更友好的 Json 数据,更有语义的数据,并且过滤一些无用数据,这里以 FToolsetTransformConverter 为例,其内部会把原 Transform 转换为 FToolsetTransform 再导出:
USTRUCT(BlueprintType, MinimalAPI)
struct FToolsetTransform
{
GENERATED_BODY()
public:
/// The world-space location.
UPROPERTY(BlueprintReadWrite, Category = "Transform")
TOptional<FVector> Location;
/// The world-space rotation.
UPROPERTY(BlueprintReadWrite, Category = "Transform")
TOptional<FRotator> Rotation;
/// The scale.
UPROPERTY(BlueprintReadWrite, Category = "Transform")
TOptional<FVector> Scale;
};
这里的结构很明显是更易于理解的,并且用了 Optional 可以避免无用导出,这里导出一下对比看下,scale 没有导出,旋转也由四元数变成了欧拉角:
{
"toolsetTransform":
{
"location":
{
"x": 100,
"y": 200,
"z": 300
},
"rotation":
{
"pitch": 1.5707963705062877,
"yaw": 2.3561945557594317,
"roll": 0.78539818525314409
}
},
"originTransform":
{
"rotation":
{
"x": -0.0065699261822421738,
"y": -0.013845038824877948,
"z": 0.020463884940290476,
"w": 0.99967313677173952
},
"translation":
{
"x": 100,
"y": 200,
"z": 300
},
"scale3D":
{
"x": 1,
"y": 1,
"z": 1
}
}
}
其他的一些现有的继承也大都如此,不一一赘述。
AI Assistant
AIAssistant 会调用 GetToolsetRegistry().GetToolsetJsonSchemas() 来获取支持的 Tool 功能。
TODO
ECABridge
FECABridgetToolset
上文已经简单提到 FECABridgetToolset,其内部通过 FECACommandRegistry 单例,调用通过 REGISTER_ECA_COMMAND 宏注册的 IECACommand,将最终调用转发到每一个 IECACommand 中。
ECAMCPServer
除此之外,ECABridge 模块还提供了 FECAMCPServer,可以监听外部 LLM 调用:
HandleToolsList 方法返回提供的工具集,这里先组装的 FECACommandRegistry 的命令,然后是 ToolsetRegistry 的:
TSharedPtr<FJsonObject> FECAMCPServer::HandleToolsList()
...
TArray<TSharedPtr<FJsonValue>> FECAMCPServer::BuildToolDefinitions()
{
TArray<TSharedPtr<FJsonValue>> Tools;
TSet<FString> ExistingNames;
for (const TSharedPtr<IECACommand>& Command : FECACommandRegistry::Get().GetAllCommands())
{
...
}
#if WITH_TOOLSET_REGISTRY
// Direction 2: Also include tools from ToolsetRegistry (excluding our own ECABridge toolset
// to avoid duplication — we already added those commands above)
AppendToolsetRegistryTools(Tools, ExistingNames); // 这里因为 ECACommand 也注册了,还会过滤掉重复的
#endif
return Tools;
}
外部调用时,在 HandleToolsCall 方法,先看 FECACommandRegistry 有无工具可执行,再将调用转发到 ToolsetRegistryTool 执行工具:
TSharedPtr<FJsonObject> FECAMCPServer::HandleToolsCall(const TSharedPtr<FJsonObject>& Params)
{
...
if (FECACommandRegistry::Get().HasCommand(ToolName))
{
...
}
#if WITH_TOOLSET_REGISTRY
// Direction 2: Try ToolsetRegistry if not found in ECABridge commands
TSharedPtr<FJsonObject> ToolsetResult;
if (TryExecuteToolsetRegistryTool(ToolName, Arguments, ToolsetResult))
{
return ToolsetResult;
}
#endif
...
}
这里还是有些冗余,因为自己的命令实际上也注册到了 FToolsetRegistry,估计代码也还会调整。
Blueprint Lisp
这里引用 FECACommand_ParseBlueprintLisp 上的注释:
/**
* Parse and validate BlueprintLisp code
*
* BlueprintLisp is a LISP-like DSL for representing Blueprint graphs in a format
* that's easy for AI to read, understand, and generate.
*
* PREFERRED: Use BlueprintLisp format for all Blueprint implementation tasks.
* It is more concise, easier for AI to reason about, and less error-prone than
* the JSON-based node commands.
*
* Example:
* (event BeginPlay
* (let player (GetPlayerCharacter 0))
* (branch (IsValid player)
* :true (PrintString "Valid!")
* :false (PrintString "Invalid!")))
*/
很清晰,Blueprint Lisp 相关类的作用就是,将蓝图与 AI 易于理解和产出的格式互换的工具,注释这里也有一个 BeginPlay 的示例。
目前 ECA 注册了四个命令,解析蓝图 Lisp,Lisp 到 蓝图,蓝图到 Lisp 和 获取 蓝图 List 帮助:
/**
* Parse and validate BlueprintLisp code
*/
class FECACommand_ParseBlueprintLisp : public IECACommand
/**
* Convert a Blueprint graph to BlueprintLisp code
*
* PREFERRED: Use this to read Blueprint logic. The Lisp format is more compact
* and easier to understand than raw node JSON.
*/
class FECACommand_BlueprintToLisp : public IECACommand
/**
* Convert BlueprintLisp code to Blueprint graph nodes
*
* PREFERRED: Use this to implement Blueprint logic. The Lisp format handles
* node creation, wiring, and positioning automatically. Much simpler than
* manually creating nodes and connections via JSON commands.
*/
class FECACommand_LispToBlueprint : public IECACommand
/**
* Get BlueprintLisp syntax help and examples
*/
class FECACommand_BlueprintLispHelp : public IECACommand
从语法可以看到这个 Lisp 十分简洁,但是也缺少很多信息,例如 Function 来自哪里,代码里转换的时候就花了一些逻辑查找:
UFunction* Function = nullptr;
TArray<UClass*> ClassesToSearch = {
UKismetSystemLibrary::StaticClass(),
UKismetMathLibrary::StaticClass(),
UKismetArrayLibrary::StaticClass(),
UKismetStringLibrary::StaticClass(),
UGameplayStatics::StaticClass(),
AActor::StaticClass(),
APawn::StaticClass(),
ACharacter::StaticClass(),
Ctx.Blueprint->ParentClass
};
// Add Geometry Script classes for procedural mesh functions
AddGeometryScriptClasses(ClassesToSearch);
for (const FString& NameToTry : FuncNamesToTry)
{
for (UClass* Class : ClassesToSearch)
{
if (Class)
{
Function = Class->FindFunctionByName(*NameToTry);
if (Function) break;
}
}
if (Function) break;
}
而对于大模型如何确认接口这种,也没有做一些统计返回之类的,可能还是要配合其他的 Command 使用,比如 FECACommand_GetBlueprintInfo 这种。
ECABlueprintLispCommands.cpp 这个文件逻辑哐哐写了 6700 行,有空再看了 TODO。
TODO 为什么要打洞 BlueprintReadOnly








Comments NOTHING