中文内容
NVIDIA ACE 是一套用于构建游戏 AI 代理的技术。ACE 为游戏内角色的各个部分提供可直接集成的云端和设备端 AI 模型,涵盖语音、智能和动画。
为了在游戏引擎旁高效运行这些模型,NVIDIA In-Game Inferencing (NVIGI) SDK 包含一组高性能库,开发者可以将其集成到 C++ 游戏和应用程序中。
NVIDIA In-Game Inferencing SDK 1.5 引入了一个新的代码代理示例,其中一个 AI 代理与玩家协作,在 2D 地牢中击败怪物。由本地小型语言模型(SLM)驱动的 AI 代理可能会对 GPU 发起过多调用,从而与图形处理竞争资源。本文探讨如何尽量减少推理调用次数,并最大化每次调用所完成的工作,从而降低图形处理与计算在 GPU 上的资源争用。
代码代理:困住幽灵
OpenAI 创始成员 Andrej Karpathy 将与大型语言模型(LLMs)协作比作召唤幽灵,这对于 LLM 智能体来说是一个贴切的隐喻,尤其是那些会编写代码的智能体。许多自定义智能体仅限于工具调用:定义一个函数,由 LLM 决定何时调用它,然后返回结果。但还有一种更具雄心的可能性。AI 智能体不仅可以调用函数,还可以创建函数及其配套代码。这使机器能够以更少的处理量实现更强的能力。
然而,这也存在取舍。不受约束且具备代码执行能力的 LLM 会带来安全问题。它可能耗尽内存、使游戏进程挂起,或者像一位不幸用户所遭遇的那样,在试图“清除缓存”时清空了硬盘。
它可能带来诸多好处,例如复杂的多步推理、动态适应,以及减少 SLM 的使用。下文将深入探讨一个潜在的编码智能体食尸鬼如何变成一个乐于助人的友善幽灵。
为什么代码智能体胜过工具调用
在讨论 AI Agent 时,最典型的用例和方法是工具调用。模型输出结构化 JSON,游戏或应用程序对其进行解析,然后执行相应的函数。虽然调用函数的能力很强大,但它只有在模型有机会稍作思考之后才会发生,而推理成本很高,尤其是当它在用户的 GPU 上争夺资源时。
一旦模型发送 JSON,它就会等待响应,再次思考,然后返回答案——这个循环可能会重复。这可能会消耗宝贵的几分之一秒,而这些时间本可以用于渲染游戏。
此外,如果函数调用周围需要复杂逻辑,系统就必须依赖较弱的模型能力。模型本身并不会处理循环;它只是生成 token。它可以尝试跟踪状态变量,但并不严谨。如果有多个项目需要处理,模型必须记住每一项,不能遗漏、重复或臆造条目。而每处理一个项目都要付出一次推理成本。
数值分析带来了另一个挑战。使用工具调用时,准确性取决于模型的数学能力,或者取决于再编写一个函数来确保正确性。
工具调用可能难以扩展。每一次函数调用都需要另一次推理命中,这会争夺 GPU 资源,因此必须加以缓解。
代码智能体的工作方式是利用计算机本就擅长的能力——运行代码。编程正成为语言模型的新兴超能力之一。与其一次生成一个函数调用,不如通过一次推理同时生成所有函数调用。初始生成之后不会再产生性能损耗,只是标准代码持续运行,直到任务完成。
它们也很灵活。虽然语言模型不容易让自身循环执行,但代码智能体可以轻松编写带有循环、计数器和过滤器的代码。以下是一个关于如何使用工具调用来瞄准敌人的假设示例。工具调用模式:
[
{
"name": "get_enemies_list",
"parameters": {
"properties": {
"position": {"type": "string", "description": "Position to search from"},
"radius": {"type": "number", "description": "Search radius"}
}
}
},
{
"name": "target_enemy",
"parameters": {
"properties": {
"enemy_name": {"type": "string", "description": "Name of the enemy to target"}
},
"required": ["enemy_name"]
}
}
]
当用户说“瞄准最近的敌人”时:
- 推理调用 1:SLM 决定调用 get_enemies_list
- 工具响应:返回 ["goblin_01", "skeleton_archer_01", "orc_chief"](仅为字符串,否则完整实体架构会撑爆上下文窗口)
- 推理调用 2:SLM 看到列表,选择其中一个,并调用 target_enemy("goblin_01")
- 工具响应:成功
- 推理调用 3:向用户反馈函数调用的状态
一次决策需要三次推理调用。考虑使用代码智能体执行同样的“瞄准敌人”动作。
代码智能体 API 定义:
get_enemies(position, radius)
--[[
Find enemies near a position.
Parameters:
position (table): Center point as {row, col}
radius (number): Search radius
Returns:
table: Array of enemy entities (with .name, .position, .health, etc.)
Example:
local nearby = get_enemies(ally.position, 10)
]]
set_target(ally, enemy)
--[[
Set an ally's attack target.
Parameters:
ally (entity): The ally to command
enemy (entity): The enemy to target
Example:
set_target(warrior, nearby[1])
]]
由 SLM 生成的用于“瞄准最近的敌人”的代码:
local enemies = get_enemies(ally.position, 10)
local closest = nil
local min_dist = math.huge
for _, enemy in ipairs(enemies) do
local dx = enemy.position[1] - ally.position[1]
local dy = enemy.position[2] - ally.position[2]
local dist = math.abs(dx) + math.abs(dy)
if dist < min_dist then
min_dist = dist
closest = enemy
end
end
if closest then
set_target(ally, closest)
end
通过一次推理调用,SLM 会遍历敌人,访问它们的位置,计算距离,并选择最近的敌人。代码智能体获得的是丰富的实体对象,而不只是字符串,并会组合出工具设计者从未预想到的逻辑。
请注意这种灵活性。同一个 get_enemies 函数可用于玩家附近、盟友附近或某个点附近的敌人。一旦 SLM 获得敌人列表,它就可以编写任何选择逻辑,例如瞄准害怕箭矢的敌人、瞄准最近的敌人,或瞄准生命值最低的敌人。使用工具调用时,适应新需求意味着需要更多工具、更多推理调用以及更高复杂度。使用代码智能体时,SLM 可以在运行时从相同的简单原语组合出新的策略。
代码智能体示例地牢
延续这种阴森主题,IGI SDK 包含一个 ASCII 地牢探索游戏,用于演示代码智能体。这个地牢包含大型游戏的所有组成部分,但采用了最简单的游戏形式之一。玩家四处移动、收集物品并与怪物战斗。但在冒险中,他们还有一位强大的盟友——AI 智能体。这种智能可以按需具现化,帮助他们战斗、执行危险任务,或提供有关前方危险的信息。

一旦给出指令,代码就会被写入,并且程序在收到新指令之前不会再次调用 SLM。工具调用链可能产生相同的结果,但代价是反复进行推理调用,消耗分配的帧时间片。
代码智能体的威胁模型
使用 SLM 生成在主机上运行的代码会带来明显的安全和安全性风险,包括:
- 危险函数访问。SLM 生成 os.execute("rm -rf /") 或 require("socket"),代码智能体随即开始删除文件或打开网络连接。
- 未授权文件访问。SLM 定位关键文件或 API 密钥,以便将其外传或删除。
- 资源耗尽。SLM 编写一个不断分配内存的循环。
- 栈溢出。SLM 编写一个没有适当终止条件的递归函数。
- 无限循环。SLM 编写 while true do end,并且永不返回。
- 逃逸沙盒。SLM 可能会操纵内部结构,以突破其隔离环境。
- 状态损坏。SLM 可能会破坏游戏或应用程序的状态。
选择目标语言
在选择目标语言时,需要考虑:执行耗时、整体性能、集成和调试的复杂性,以及生成代码的质量与安全性。
在运行游戏时,推理调用必须只占总帧时间的一小部分。导致渲染管线停顿的大幅耗时是不可接受的。虽然可以在每一帧生成少量 token,以平滑推理过程,但编译并不具备这种灵活性。这就排除了 C++ 或 C# 等编译型语言。相反,需要一种解释型语言。
有两种语言可作为典型选择:Python 和 Lua。
Python 显然是首选。SLMs 能够流畅地生成 Python。其生态系统非常庞大。但 Python 并不是为嵌入或沙箱化而设计的。全局解释器锁(GIL)会使多线程宿主程序变得复杂。隔离需要使用子进程或子解释器,而这两者都会增加复杂性。此外,Python 没有内置方式来限制内存或执行时间。Python 可以在沙箱中运行,但这一路上都需要与这门语言的特性相抗衡。
Lua 从一开始就是为嵌入到恶劣环境中而设计的。整个运行时约为 200 kB,并且能在亚毫秒级时间内启动。此外,每个已识别的威胁都有记录在案的缓解措施,包括:
- 危险函数:选择性加载库。不要加载 io 或 os,这样它们就不存在。
- 内存耗尽:自定义分配器钩子。跟踪每一次分配,并强制执行上限。
- 栈溢出:函数调用上的调试钩子。统计深度,溢出时报错。
- 无限循环:基于指令计数的调试钩子。在执行 N 条指令后报错。
- 元表操作:从全局环境中移除 getmetatable/setmetatable。
- 状态损坏:使用自定义 __newindex 元方法,拒绝对受保护字段的写入。
因此,Lua 满足了该 IGI 示例的所有要求,但仍需要加固。使用 Lua 时,可将危险或不需要的函数设为 nil(lua_pushnil(L); lua_setglobal(L,"funcname") 模式)。通过包装默认分配器并跟踪分配情况来限制内存增长。程序员可以设置钩子(lua_sethook)以确保程序不会导致调用栈溢出或无限期挂起。同样,可以通过锁定自定义元方法来限制元表访问,以保护游戏状态。
这些只是为锁定此示例所采取的部分步骤。可能还需要更多措施(取决于每个具体游戏或使用场景),但这些提示应有助于引导读者查看代码。
为增强安全性,可以将 Lua 嵌入 WebAssembly 运行时中。有关保护智能体行为的更多方法,请参阅博客文章 Sandboxing Agentic AI Workflows with WebAssembly 和 Practical Security Guidance for Sandboxing Agentic Workflows and Managing Execution Risk。
安全是核心关切,而不是事后考虑。语言选择是一项安全决策,而不是便利性决策。以这一前提为出发点,理解需要防范的不同攻击向量,幽灵就会继续成为机器中的朋友。
开始使用 NVIDIA In-Game Inferencing SDK
试用 NVIDIA In-Game Inference SDK 示例。构建它、进行实验,并思考如何将其应用于游戏、应用程序和其他项目。
加入我们,共赴 GDC
探索 NVIDIA RTX 神经渲染和 AI 如何塑造游戏的下一个时代。NVIDIA 开发者与性能技术副总裁 John Spitzer 将展示路径追踪和生成式 AI 工作流的最新创新,带你一窥游戏开发的未来。
加入 NVIDIA 应用深度学习研究副总裁 Bryan Catanzaro 的互动式“Ask Me Anything”问答环节,了解 AI 的最新趋势。
标签














