摘要:Latitude 是一个开源 AI 工程平台,优先实现观测性和评估,通过捕获生产流量中的提示、输入/输出、工具调用、延迟、令牌使用量和成本数据,建立评估驱动的可靠性循环,帮助团队从现有 LLM 调用开始,逐步迭代提示、版本管理、实验对比,并将失败转化为可重复的修复。平台采用 monorepo 架构、Drizzle ORM、BullMQ 作业、OpenTelemetry 追踪和 PromptL 链执行,支持云托管与自托管部署。本文基于官方文档,详细阐述贡献哲学、构建测试、Docker 生产构建、代码模式、数据库迁移、提示运行架构等实践,为专科及以上学历开发者提供准确、可操作的指导。
Latitude LLM:开源 AI 工程平台的架构设计与开发实践指南
你是否正在构建 LLM 驱动的应用,却苦于缺少系统化的观测、评估和持续优化机制?Latitude 提供了一个完整的开源解决方案。它不是简单的提示管理工具,而是围绕生产流量构建的工程平台:先通过观测捕获真实运行数据,再用评估量化质量,最后形成可靠性循环,将问题转化为可重复的修复。本文将带你一步步了解平台的整体设计、贡献流程和技术实现细节,帮助你高效参与或部署这个平台。
为什么选择 Latitude?
Latitude 的设计理念是分阶段采用:从观测和评估起步,逐步进入可靠性循环。
观测阶段:捕获真实流量中的提示模板、输入参数、输出结果、工具调用详情、延迟、令牌消耗和成本指标。这些数据直接来自生产环境,帮助你重现问题。
提示游乐场:使用真实输入重现运行过程,迭代提示内容、版本变更,并通过 AI 网关发布更新。
数据集管理:从生产日志中提取真实示例,构建批量测试集和回归测试套件。
评估系统:内置评估规则、LLM-as-judge 以及人工打分机制,支持实时评估。
实验功能:对比不同模型提供商、提示版本的效果,通过量化指标(如成功率、成本、延迟)选择最佳方案。
进入可靠性循环后:
-
标注功能:将人工判断转化为可追踪的信号。 -
问题发现:聚类失败案例,识别重复出现的故障模式。 -
自动评估:将发现的问题转化为持续测试,守护每次发布。 -
提示优化器(GEPA):在评估套件上搜索提示变体,降低重复失败率。
Latitude Telemetry 支持多数模型提供商和框架,可通过 OTLP 扩展自定义集成。这种渐进式路径让团队避免大刀阔斧的重构,从现有代码开始逐步提升可靠性。
快速开始 Latitude
Latitude 提供两种部署方式:云托管版和自托管版。
云托管版(Latitude Cloud):
-
在 latitude.so 注册账号并创建项目。 -
添加 Telemetry SDK 或导出 OTLP 追踪。 -
创建数据集和评估规则,测量质量并捕获回归。 -
版本化提示/代理,通过网关发布变更。 -
利用评估驱动优化减少重复失败。
自托管版:
按照自托管生产部署指南安装后,重复上述云版步骤即可。平台支持 Docker Compose 快速启动数据库、Redis、Weaviate 等依赖。
平台核心优势在于观测数据的闭环利用:生产日志 → 数据集 → 评估 → 优化 → 重新部署,形成持续改进循环。
贡献哲学:长远思维与代码卫生
贡献 Latitude 时,请牢记核心理念:这个代码库将比你更长寿。任何快捷方式都会成为他人负担,任何 hack 都会积累技术债务,拖慢整个团队。
你不仅在编写代码,更在塑造项目未来。你的模式会被复制,你的捷径会被重复。必须对抗熵增,让代码库比你接手时更好。
具体要求:
-
优先使用 TypeScript,倾向类型而非接口。 -
采用函数式编程模式,早返回提升可读性。 -
使用描述性命名,如 isLoading、hasError。 -
事件处理函数以 handle 开头,例如 handleClick。 -
目录名小写连字符,如 auth-wizard。 -
避免枚举,使用 const 映射或类型联合。 -
仅为导出的函数和类添加 JSDoc 注释,内部简单函数无需注释。 -
导出置于文件顶部,内部方法置于底部。 -
错误处理优先使用 instrumentation 的 captureException,而非 console.error。 -
仅在需要 JSDoc 或被明确要求时添加注释。
这些规范确保代码库长期可维护,避免短期便利带来的长期成本。
构建与测试命令
项目使用 pnpm、Turborepo 和 Vitest 管理多包 monorepo。
常用命令:
-
pnpm build:构建所有包(仅在明确要求时使用)。 -
pnpm dev:启动开发服务器。 -
pnpm lint:检查所有包。 -
pnpm tc:类型检查所有包。 -
pnpm test:运行所有测试。 -
pnpm test:watch:监视模式运行特定包测试。 -
pnpm --filter @latitude-data/core db:migrate:运行数据库迁移。 -
pnpm --filter @latitude-data/core db:generate:生成迁移文件。 -
pnpm prettier:格式化代码。
测试时避免直接使用 vitest 命令,应通过 pnpm test 或在 package.json 添加相应脚本。
Docker 生产构建实践
生产部署采用 Docker Compose,支持三种配置文件:
-
docker-compose.yml:使用 GHCR 预构建镜像的生产部署。 -
docker-compose.local.yml:从源码本地构建开发镜像。 -
docker-compose.prod.yml:带 Traefik 反向代理的生产部署。
本地构建 Web 应用镜像:
# 启动依赖(数据库、Redis、Weaviate)
docker compose -f docker-compose.local.yml up db redis weaviate -d
# 构建 web 容器
docker compose -f docker-compose.local.yml build web
# 运行 web 容器
docker compose -f docker-compose.local.yml up web
应用运行于 http://localhost:3000。
Web Dockerfile 关键配置:
-
基础镜像:node:22-alpine -
构建工具:Turbopack(next build –turbopack) -
输出模式:standalone(next.config.mjs 中设置 output: ‘standalone’) -
多阶段构建:Pruner → Builder → Runner,减少镜像体积
构建参数:
-
NEXT_PUBLIC_*:客户端环境变量(编译时烘焙) -
AWS_REGION、S3_BUCKET、BUILD_ID:静态资源上传至 S3 -
DD_GIT_COMMIT_SHA:Datadog source map 上传
测试构建镜像:
docker run --rm --network llm_default --env-file .env -p 3000:8080 llm-web
或继续使用 docker compose。
其他服务构建:
-
docker compose -f docker-compose.local.yml build(全部) -
docker compose -f docker-compose.local.yml build gateway workers websockets(指定服务)
代码风格与架构概述
项目采用 pnpm 工作区 + Turborepo monorepo 结构。
-
核心业务逻辑位于 packages/core -
UI 组件位于 packages/web-ui -
服务层采用函数式风格,返回 Result 抽象处理错误 -
数据库操作使用 Transaction 抽象,模型位于 packages/core -
写入操作接收可选 db 参数,默认 database -
更新/删除服务接收模型实例而非 ID
测试文件与源码并列,使用 .test.ts 后缀。
测试模式详解
测试优先使用工厂函数,集成测试尽量减少 mock。
运行特定包测试:
进入包目录后:
-
pnpm test -- "path/to/file.test.ts" -
pnpm test -- "path/to/directory" -
pnpm test(包内全部)
单元测试外部依赖模式:
-
使用 vi.spyOn 模拟模块:
import * as cacheModule from '../../cache'
import * as diskModule from '../../lib/disk'
beforeEach(() => {
vi.spyOn(diskModule, 'diskFactory').mockReturnValue(mockDisk as any)
vi.spyOn(cacheModule, 'cache').mockResolvedValue(mockCache as any)
})
-
创建 vi.fn() mock 对象,定义所有使用方法。 -
beforeEach 清空 mock 并重置。 -
测试 Result 对象:success 检查 ok 和 value,error 检查 ok 和 error.message。 -
覆盖边缘情况:缓存缺失、过期、外部服务失败、静默错误处理。 -
使用 @ts-expect-error 处理 mock 类型不匹配。
测试文件结构:
import { beforeEach, describe, expect, it, vi } from 'vitest'
describe('moduleName', () => {
const mockDependency = { method: vi.fn() }
beforeEach(() => {
vi.clearAllMocks()
})
describe('functionName', () => {
it('describes expected behavior', async () => {
// Arrange
mockDependency.method.mockResolvedValueOnce(value)
// Act
const result = await functionUnderTest(args)
// Assert
expect(result).toEqual(expected)
expect(mockDependency.method).toHaveBeenCalledWith(expectedArgs)
})
})
})
这种结构使测试清晰、可维护,并全面覆盖成功、失败和边缘路径。
CRUD 操作模式
服务层(packages/core/src/services/):
-
每个实体创建独立文件夹,如 apiKeys/、providerApiKeys/ -
文件:create.ts、destroy.ts、update.ts -
使用 Transaction 和 Result,返回命名导出 -
避免 barrel 文件导出全部服务
动作层(apps/web/src/actions/):
-
使用 authProcedure + Zod 验证 -
先通过仓库获取模型实例,再调用服务 -
管理后台动作置于 actions/admin/
示例:
export const updateApiKeyAction = authProcedure
.inputSchema(z.object({ id: z.number(), name: z.string() }))
.action(async ({ parsedInput, ctx }) => {
const repo = new Repository(ctx.workspace.id)
const model = await repo.find(parsedInput.id).then((r) => r.unwrap())
return updateService(model, { name: parsedInput.name }).then((r) =>
r.unwrap()
)
})
项目作用域示例使用 withProject 过程。
存储层(apps/web/src/stores/):
-
使用 SWR 自定义钩子 -
动作成功后乐观更新 -
导出 create、destroy、update 及 loading 状态 -
成功/错误时显示 toast
UI 模式:使用模态框编辑,表格单元格按钮带 tooltip,图标统一(edit、trash)。
数据库模式与迁移
使用 Drizzle ORM + PostgreSQL。
模式定义(packages/core/src/schema/models/):
export const tableName = latitudeSchema.table('table_name', {
id: bigserial('id', { mode: 'number' }).notNull().primaryKey(),
name: varchar('name', { length: 256 }).notNull(),
workspaceId: bigint('workspace_id', { mode: 'number' })
.notNull()
.references(() => workspaces.id, { onDelete: 'cascade' }),
...timestamps(),
})
破坏性迁移要求(两步 PR):
PR1(先部署):移除代码引用、更新 Drizzle 模式、仓库、服务、工厂、测试。旧列仍存在但未使用。
PR2(后部署):生成并运行迁移删除未用列/表。
仓库模式:
继承 RepositoryLegacy 或独立类,实现 scope getter 过滤 workspace。
ClickHouse 迁移
ClickHouse 用于分析和高性能存储,迁移使用 golang-migrate。
命令:
-
pnpm --filter @latitude-data/core ch:connect -
pnpm --filter @latitude-data/core ch:status -
pnpm --filter @latitude-data/core ch:create <name> -
pnpm --filter @latitude-data/core ch:up -
pnpm --filter @latitude-data/core ch:down [N|all] -
pnpm --filter @latitude-data/core ch:reset
两个迁移文件夹:
-
unclustered/:单节点(开发、自托管) -
clustered/:复制集群(生产)
必须为每个迁移同时创建并保持同步两种版本。
unclustered 示例:
CREATE TABLE events (
id String,
workspace_id UInt64,
timestamp DateTime64(3)
) ENGINE = ReplacingMergeTree()
ORDER BY (workspace_id, timestamp, id);
clustered 示例:
CREATE TABLE events ON CLUSTER default (
id String,
workspace_id UInt64,
timestamp DateTime64(3)
) ENGINE = ReplicatedReplacingMergeTree()
ORDER BY (workspace_id, timestamp, id);
规则:
-
始终创建两种版本 -
逻辑完全一致,仅语法差异 -
提供可逆 down.sql -
使用 ReplacingMergeTree 支持 upsert -
查询包含 workspace_id 实现租户隔离
API 路由模式
路由置于 apps/web/src/app/api/[endpoint]/route.ts
使用 errorHandler(authHandler(…)) 保护路由。
示例:
export const GET = errorHandler(
authHandler(
async (_: NextRequest, { workspace }: { workspace: Workspace }) => {
const repo = new Repository(workspace.id)
const data = await repo.findAll().then((r) => r.unwrap())
return NextResponse.json(data, { status: 200 })
}
)
)
公共路由仅用 errorHandler。
作业模式(BullMQ)
作业用于后台任务,如导出、评估、文档运行。
要求:
-
成功时返回 undefined -
可重试错误直接 throw -
不可重试错误使用 captureException -
配置 removeOnComplete: true,removeOnFail: false -
在 packages/core/src/jobs/index.ts 注册
错误处理示例:
-
throw 触发重试 -
captureException 处理单个项失败但作业继续
后台管理(Backoffice)模式
-
添加路由至 BackofficeRoutes 枚举和 ROUTES.backoffice -
页面置于 app/(admin)/backoffice/[section]/page.tsx -
使用 Text.H1 标题、Text.H4 描述 -
组件置于 _components/ 子目录 -
避免 barrel 导出
UI 规范:
-
从 @latitude-data/web-ui/atoms/ 导入 -
使用 Modal、TextArea -
按钮 size=’small’ -
表格组件:Table、TableBody 等 -
读操作用 API 路由 + useFetcher + SWR -
写操作用 server actions + useLatitudeAction
特征实现检查清单
新功能按以下顺序:
-
数据库模式 + 生成迁移 -
服务层(Result 模式) -
仓库层 -
动作层(admin/ 用于后台) -
API 路由 -
存储层(SWR 乐观更新) -
UI 组件 -
路由与导航
SDK 发布流程(TypeScript SDK)
修改后必须:
-
更新 packages/sdks/typescript/package.json 版本(semver) -
更新 CHANGELOG.md(Added/Changed/Fixed 等节) -
推送到 main 触发 CI/CD:构建、npm 发布、GitHub release、打标签
作业与事件系统
事件声明:
-
添加至 Events 联合类型 -
定义 LatitudeEventGeneric -
添加至 LatitudeEvent -
(可选)添加至 IEventsHandlers
发布:
使用 publisher.publishLater 或 publish。
事件处理:在 packages/core/src/events/handlers/index.ts 注册。
提示运行架构详解
提示运行系统支持前台流式和后台队列执行。
高层流程:
API 请求 → Gateway → runDocumentAtCommit → ChainStreamManager → AI Provider → 响应
同时支持后台 enqueueRun → BullMQ 作业,并记录 OpenTelemetry 追踪。
核心服务:
-
runDocumentAtCommit:构建提供商映射、解析内容、校验链、委托 runChain。 -
ChainStreamManager:递归 step 执行、工具解析、流式响应。 -
ai() 服务:应用提供商规则、创建 Vercel AI SDK 适配器、streamText。
后台执行:
enqueueRun 创建 Redis 记录并入队,backgroundRunJob 处理作业、启动追踪、转发流事件至 Redis stream。
追踪:
使用 OpenTelemetry,Span 类型包括 Prompt、Completion、Tool、Step 等。
Span 处理包含 prompt 参数、模板、引用等元数据。
评估集成:
spanCreated 事件触发 evaluateLiveLogJob,对符合条件的日志运行评估,生成 EvaluationResultV2。
PromptL 链:
编译为 Chain 对象,支持 step() 逐步执行,处理 userMessage、参数验证、文件类型转换。
工具处理:
支持客户端工具、内置工具、MCP 集成、代理子提示,通过 lookupTools 和 resolveTools 解析。
这种架构确保生产级可观测性、可扩展性和错误恢复能力。
FAQ
Q: Latitude 如何处理破坏性数据库变更?
A: 分两步 PR。第一步移除代码引用并更新模式,部署后第二步生成并运行删除迁移,避免旧代码崩溃。
Q: 如何为新功能添加评估?
A: 在 evaluationsV2/specifications/ 实现 run() 和 supportsLiveEvaluation,支持 LLM-as-judge、规则或自定义指标。Live 评估通过 spanCreated 事件自动触发。
Q: 后台作业错误如何重试?
A: 可重试错误直接 throw 让 BullMQ 重试,不可重试错误使用 captureException 记录但继续执行。
Q: ClickHouse 迁移需要注意什么?
A: 必须同时维护 unclustered 和 clustered 两个版本,仅 ON CLUSTER 和引擎前缀不同。使用 ReplacingMergeTree 并包含 workspace_id。
Q: 如何在 UI 中实现乐观更新?
A: useLatitudeAction 的 onSuccess 内调用 mutate 更新 SWR 数据,同时显示 toast 反馈。
Q: 提示链如何支持工具调用?
A: ChainStreamManager 通过 resolveTools 准备工具,streamAIResponse 处理请求/完成事件,支持多步链执行。
Q: 自托管时如何选择 ClickHouse 模式?
A: 根据 CLICKHOUSE_CLUSTER_ENABLED 环境变量自动选择 unclustered 或 clustered 迁移文件夹。
通过以上实践,Latitude 构建了一个可观测、可评估、可优化的 LLM 工程平台。无论你是贡献代码还是部署生产,都能从中获得清晰的路径和可靠的工具支持。欢迎加入社区,一起推动 AI 工程化发展。
