
本文字数:10316;估计阅读时间:26分钟
作者:Tobias Wochinger
编者按: 本文译自 Langfuse 原博客。 原文围绕「探讨在多租户云原生环境下,如何平衡代码评估器的灵活性与底层系统的安全性。」展开。文章深入剖析了在核心数据存储环境中执行第三方代码的风险控制逻辑。对于构建可扩展且安全的 LLM 监控工具具有较高的参考价值。
代码评估器允许你使用自己的 Python 或 TypeScript 代码为 trace 评分。以下介绍其背后的执行模型:我们的具体需求、放弃的备选方案,以及所采取的安全策略。
5 月底,我们发布了代码评估器:只需编写少量的 Python 或 TypeScript 函数,Langfuse 就会运行它来为 trace 评分。从设计上看,这实际上意味着任何人都可以在我们的多租户 SaaS 环境中运行不可信代码,而该环境存储着数千个团队 PB 级别的核心数据。
文章梳理了我们为其设计运行时环境的过程、期间排除的方案,以及第一批用户试图攻破该环境时发生的情况。

为什么需要代码评估器
评估是检验 LLM 应用质量的手段。人工阅读 trace 是你建立对应用认知的过程,这无可替代。但没有人会手动阅读数百万条 trace。错误分析会将这种阅读结果转化为错误模式列表,而常规做法是将每种模式编写为 LLM-as-a-judge 评估器,通过它对每条 trace 的 Agent 实用性、简洁度或语气等指标进行评分。
然而团队往往需要检查许多毫无主观色彩的内容。输出是否为有效的 JSON?回答是否与预期值匹配?对于这类几行代码每次都能完美解决的问题,使用模型反而是一种昂贵且缺乏确定性的方式。
代码评估器的作用就是解决此类问题:你提供用于评估观测数据的 Python 或 TypeScript 代码,配置触发时机,随后 Langfuse 会为你进行大规模的运算。
让我们看一个实际的评估器示例:
function evaluate({
observation: { output },
}: EvaluationContext): EvaluationResult {
const required = ["summary", "items", "sections"];
let missing = required;
try {
const parsed = typeof output === "string" ? JSON.parse(output) : output;
missing = required.filter((key) => parsed?.[key] == null);
} catch {
// output is not valid JSON, so all fields count as missing
}
return {
scores: [
{
name: "has-required-fields",
value: missing.length === 0,
dataType: "BOOLEAN",
comment: missing.length
? `Missing: ${missing.join(", ")}`
: "All fields present",
},
],
};
}
带有严格限制的小型工作负载
如上例所示,代码评估器属于限制较强的工作负载:它们执行时间短(通常为数百毫秒)、具备确定性(输入相同则评分相同),且无状态(没有文件系统,多次运行之间不保存状态)。
这与目前随处可见的 Agent 沙箱(如 E2B、Modal)形成了鲜明对比:后者的存在是为了提供长生命周期的环境、文件系统访问权限、快照功能以及在运行时执行 ~~pip install~~ uv add。而许多评估器根本不需要这些。
我们真正需要的是:
• 安全性: 无论是我们的 Langfuse Cloud 环境,还是平台团队为多个内部团队托管的 Langfuse,租户之间都绝不能访问彼此的数据。
• 规模: 我们每天接收数亿条观测数据,每一条都可能触发一个或多个代码评估器。无论底层用什么机制运行它们,都必须在这种数据体量下保持成本可控。
• 自托管体验对等: Langfuse 一向易于自托管,我们也希望保持这一优势。运行该功能并保障其安全性,不应额外依赖专门的基础设施团队。
• Python 与 TypeScript 支持: 用户希望用自己日常工作的语言进行开发。Python 深受数据科学家的青睐,而 TypeScript 正逐渐成为 AI 应用工程师的行业标准。
在 Langfuse,我们坚信要 快速且频繁地发布:尽早把功能交到用户手中,从他们的反馈中学习,并在为其提供运维服务的过程中积累经验。
对于代码评估器来说,精简发布意味着刻意不包含两项内容:网络访问和第三方依赖。虽说这两者在评估器中都有合理用途,但我们要确保未来有明确的规划,将其作为经过严密设计的受控功能添加进来。
我们考虑过的方案
为不受信任的代码提供沙箱运行环境并非新问题,因此我们有多种方案可供选择:
分发到用户的基础设施
像 Claude Code 这样的工具直接在用户本地机器上运行,从而完全避开了这类问题:既不需要构建租户隔离,又能完全访问用户环境。对我们而言,类似的做法是提供 Webhook:由 Langfuse 调用用户托管的接口,并从响应中提取评分。但这样一来,成本就转嫁给了用户:每个人都需要搭建、扩容并监控一个每秒处理数百次请求、每次附带 MB 级数据载荷的服务,而我们开发这项功能,本意就是为了帮用户省去这些麻烦。
提供自定义 DSL
如果各种检查项都高度相似且具确定性,何必还要提供一门完整编程语言的全部功能?用一种小型的领域特定语言(DSL)或查询构建器,不就能保证只生成安全代码了吗?应用场景或许相似,但数据却千差万别。尽管 DSL 能满足大部分工作负载,但在遇到不符合其规则的长尾检查需求时,用户就会陷入死胡同,找不到任何变通方案。而且,对于人类或智能体(agents)而言,自定义 DSL 并不在他们已有的知识或训练分布中。
进程内隔离
快速启动且无需额外基础设施组件:进程内隔离(如 V8 isolates、wasmtime、seccomp、Deno 或 Pyodide)本是次优解,特别是在兼顾自托管需求的情况下。但这套方案很难做到万无一失,因为安全控制机制与不可信代码运行在同一个进程中。无论权限检查、系统调用过滤还是运行时自身,只要出现一个漏洞,攻击者就能打入内部环境,窃取我们的服务凭据、网络权限及其他客户数据。这里没有第二道防线。大规模运用此模式时,Cloudflare 在 isolates 之上叠加了进程沙箱,并保持着 24 小时内更新 V8 补丁的频率;而 n8n 的 Pyodide 沙箱依然因黑名单机制被绕过而遭到逃逸(CVSS 9.9)。我们不愿承担这样的风险,更不愿把风险转嫁给自托管用户。
Firecracker, gVisor, Kata
这些是业界顶级的解决方案,支撑着 AWS Lambda(Firecracker)以及网页版 Claude Code 和 OpenAI 的代码执行功能(均使用 gVisor)。遗憾的是,它们的部署成本很高:Firecracker 和 Kata 需要 KVM,这意味着必须使用裸金属服务器或嵌套虚拟化,而 gVisor 则自带容器运行时。对我们自己来说,一次性配置尚可接受,但这并不是我们想让客户承担的工作。
Kubernetes Jobs
将代码执行任务调度为 Kubernetes Jobs 非常契合自托管场景。对于不愿依赖大型云服务商作为后端的自托管用户而言,这仍是极佳的选择。然而,为了几百毫秒的工作负载去调度整个 pod 过于沉重且开销极大:调度和容器启动会给每次运行增加数秒延迟;在每秒数百次执行的并发下,单是 Job 的频繁创建与销毁,就足以对控制平面构成压力测试。此外,普通容器共享宿主机内核,在真实的高风险多租户环境中,底层仍需搭配 gVisor 或 Kata。
我们的运行时选择:AWS Lambda
AWS Lambda 与我们的问题需求几乎完美契合。它正是作为服务运行的 Firecracker——也就是我们刚刚排除由自己运维的顶级方案:每次调用都在一个微型虚拟机(microVM)中进行,启动时间约 100 毫秒,专为高并发工作负载打造。
评估这些方案后,我们也改变了对自托管的立场。在与不同规模客户沟通后,我们清楚地认识到,并不存在放之四海而皆准的后端方案,即便是 Kubernetes 也不例外。因此,我们没有选择向最低标准妥协,而是将后端设计为可插拔式,并选择 AWS Lambda 作为首个实现:
export interface CodeEvalDispatcher {
name: string;
dispatch(input: DispatchInput): Promise<DispatchResult>;
}
自托管用户可以使用自己的 AWS 凭证来运行,在开发和受信任环境中,也可以回退使用内置的本地调度器(dispatcher)。
租户隔离与热环境
Lambda 的一大吸引力在于其安全模型相对可控:一切权限默认封闭,任何放宽权限的决定都需要刻意配置。不过它仍有一处不足:租户隔离。
连续的 Lambda 调用之间并没有做到真正的隔离。一次“冷”调用会获取一个全新的微虚拟机(microVM);而后续的“热”调用则可能复用同一个虚拟机,甚至包含上一次运行残留在内存或 /tmp 目录中的数据,这就带来了跨租户数据泄露的潜在风险。
直到去年底,唯一的解决办法仍是为每个租户分配独立的 Lambda 函数(在我们的场景中,甚至是每个租户分配两个 Lambda,对应两种编程语言)。AWS 允许这种做法,但生命周期管理的复杂性必须由我们自己承担:比如在创建租户时动态创建函数,在函数闲置或 Langfuse 项目被删除时进行销毁。最糟糕的是,我们还要确保每个函数内部的微型执行框架保持同步更新。
幸运的是,AWS 推出了租户隔离功能:在每次调用时传入一个租户标识,Lambda 就能保证执行环境绝不会在不同租户间复用。我们以项目作为隔离标识,这本身也是 Langfuse 中的数据隔离边界。这里存在一个可接受的折中:同一项目下的两个评估器(evaluator)可能会共享同一个热环境,但这并不会泄露任何超出该项目可见范围的数据。
调度路径:同步与内联
使用两个 Lambda 也意味着每次请求都要附带用户的代码——考虑到评估器本身只是很短的代码片段,这一点是可以接受的。在处理了数亿次 LLM-as-a-judge 调用后,我们对请求负载(payload)大小和吞吐量有了准确把握,因此最初采用了最精简的方案:同步调用,并将评估负载与源代码直接内联。随着使用量的增长,通过 SQS 进行异步调用并结合预签名的 S3 URL,能为我们提供一条清晰且(最重要的是)向后兼容的平滑升级路径。
Lambda 内部有一个由 Langfuse 提供的微型脚手架(harness),负责解析请求负载并调用用户的代码:

最小权限原则
既然已经具备了安全的执行环境,接下来只需确保在接入环节不出纰漏。对我们而言,这意味着严格遵循最小权限原则:未传递的凭证、未执行的操作、未添加的能力,就绝不会引发安全问题。
具体而言:
• 将代码视为数据。 我们仅在客户端或 Lambda 内部与代码交互。一旦代码进入后端,便会被视为不可解析的普通字符串。这也意味着可以省去 TypeScript 求值器的所有转译步骤:我们仅允许使用 可擦除的 TypeScript 语法 ,并由运行框架在 Lambda 内部将其剥离。
• 最小化 Lambda 权限。 环境中仅保留执行角色自身的凭证,且该角色除了写入日志外没有其他任何权限(从技术层面讲,该角色还附带了 Lambda 管理 VPC 网络接口所需的 EC2 权限,但 我们拦截了从执行环境内部发起的相关调用 )。
• 禁止出站。 Lambda 挂载在独立的 VPC 中,配有专用的安全组,且无法访问内外网。这么做主要是为了防止算力被滥用于攻击第三方,而非担心数据泄露。至于带有限流功能和白名单的出站访问,后续可作为独立特性另行添加。
• 严格的配额限制。 为防止滥用,执行时间(2 秒)、源码大小(256 KB)、输入载荷(5.5 MB)以及返回结果(256 KB)均设有上限。
计划步入生产
得益于详尽的前期规划、与 ClickHouse 安全团队共同开展的威胁建模,以及基于优秀的 floci AWS 模拟器构建的全面测试套件,我们对系统实现的健壮性充满信心。但常言道,计划本身一文不值,做计划的过程才最重要:我们深知代码求值器一旦上线,必然会立刻迎来各种试探。事实也的确如此。
发布几天后,一位安全研究员联系了我们,声称可以通过读取环境变量并将其作为评分注释返回,从而从求值器运行时中提取出数据库凭证。关于数据库的说法其实并不成立:如前文所述,我们不会向执行环境传入任何凭证。但这套信息外传通道本身确实行得通,而这也正是我们事先评估过并已做好防范的攻击向量(vector)。
必须承认,我们的设计中存在一个盲点:这些环境变量中还包含 AWS 为 Lambda 执行角色自行注入的凭证。在收到最初报告的几小时后,有人开始使用窃取到的凭证探测 AWS API:

对于凭证提取本身,其实没有太多防范手段:这些凭证由 AWS 预留,即使去擦除 os.environ 或 process.env,也只会增加提取难度,无法完全杜绝。你可以在进程内增加沙盒机制来进行纵深防御,但这仍无法完全阻止凭证被提取,反而会带来极大的复杂度和性能开销。
最终,最小权限原则发挥了作用:几分钟内,GuardDuty 就监测到有人试图从外部 IP 使用该 runner 凭证,并确认所有相关请求均被拒绝:

我们的防线守住了。
后续计划
在 v1 版本中,我们刻意舍弃了部分功能,以换取可验证的特性:无外网出口、无依赖项、无自定义凭证。作为交换,我们获得了开箱即用的租户隔离机制,即便 runner 的凭证被盗也毫无用处。最重要的是,我们迅速将这项功能交到了真实用户手中,并开始不断接收反馈。
如果你想尝试 code evaluator,可以从阅读文档开始(https://langfuse.com/docs/evaluation/evaluation-methods/code-evaluators)。如果本文档提到的某些设计限制了你的使用,请在 GitHub Discussions 中告诉我们:我们后续增加的新功能,取决于你们的实际需求。
另外,如果为成千上万的租户运行不受信的代码对你来说是个有趣的挑战,我们正在招聘(https://langfuse.com/careers)。
ClickHouse是面向 AI 时代打造的高性能实时分析数据库,能够以极致性能处理海量数据分析任务。凭借高并发、低延迟和云原生架构,ClickHouse 广泛应用于可观测性、数据仓库、实时分析及 AI 数据基础设施等场景。我们致力于帮助企业在公有云平台上构建安全、弹性且高性价比的实时分析与 AI 数据平台,加速释放数据价值,推动智能化创新与数字化转型。目前,Trip.com、DiDi、Meta、Sony、Netflix、Deutsche Bank、Sierra、Cloudflare 等全球领先企业均在使用 ClickHouse 支撑其关键业务和数据分析平台。
470



被折叠的 条评论
为什么被折叠?



