LLM调用日志规范:字段设计、成本核算与可观测实践 这次我们来聊一个 LLM 工程落地过程中很容易被忽略但排查问题、核算成本时又绕不开的问题LLM 调用应该记录什么模型名、提示词、返回内容、token 用量、响应耗时、失败原因、链路 ID……不同团队各记各的字段经常对不上。项目名称虽然叫 No standard says what to record about an LLM call, so I built the record但本质上是同一件事没有人定义 LLM 调用日志的字段规范所以作者自己造了一套可复用的record数据结构。这个项目/方案最核心的地方不是接入方式多复杂而是回答了几个实际问题每次调用最少需要记录哪些字段哪些字段用于排障哪些字段用于算成本哪些字段必须脱敏调用信息怎么和业务链路、Agent 多轮调用串联起来记录结果怎么批量导出方便后续做可视化分析本文会围绕这套 record 设计展开给出核心字段定义、Python 环境下的接入模板、请求响应的自动捕获方式、JSONL 批量导出、接口服务示例以及常见排障和合规注意事项。适合正在做 LLM 应用开发、维护 LLM Agent、接入 RAG 流程、或者想把 LLM 调用日志纳入公司可观测体系的开发者阅读。1. 核心能力速览能力项说明项目类型LLM 调用记录标准与日志采集工具解决的问题LLM API 调用缺少统一日志字段排障、成本核算、审计困难记录维度请求、响应、token 用量、延迟、状态、成本、链路 ID、用户标识输出格式JSON、JSONL、CSV可接入日志系统或对象存储运行环境Python 3.9Linux / macOS / Windows 均可启动方式作为 Python 库引入也可独立部署日志接收服务接口 API可提供 REST 接口接收调用日志批量任务支持批量写入、队列缓冲、采样记录推理硬件不依赖 GPU日志记录本身开销小适用场景生产环境调用监控、成本统计、错误排查、合规审计这里的数值和细节需要以实际代码版本为准。项目本身不关心你是用 OpenAI、Anthropic 还是本地部署的模型只要请求和响应能被拦截就能按统一 record 结构落盘。2. 为什么 LLM 调用需要一份 record 标准很多团队刚开始接 LLM API 时日志都是随手打的。有人记录model prompt有人记录response status还有人只在出问题时打印异常。短期看没问题等接口并发量上来或者接入 LLM Agent 后问题就开始暴露排障难一次 Agent 多轮调用可能涉及 5 到 10 次模型请求没有conversation_id或trace_id根本串不起来。成本难算token 用量不统一记录月底账单出来才知道花了多少钱没法按模块、按用户分摊。效果难复盘想分析哪个提示词版本效果好缺少历史请求和响应样本只能凭感觉调整。审计缺失涉及业务数据的 LLM 调用如果日志里没有用户标识和授权信息后续合规检查会非常被动。OpenTelemetry 社区已经有 GenAI semantic conventions 草案LangSmith、Langfuse 这类平台也有自己的日志模型但自研项目要快速接入这些平台通常需要额外依赖。对于大多数团队来说轻量做法是先定义自己的 record 数据结构把每次 LLM 调用的核心信息结构化落盘后续再考虑是否同步到第三方平台。这个 record 方案适用的场景很明确已经有 LLM 调用代码想在不改业务逻辑的情况下增加完整调用记录或者正在设计新的 LLM 服务希望第一步就把日志模型建好。不适合的场景是想直接获得完整可视化链路追踪平台那还是选择成熟商业产品或开源可观测系统更省力。3. LLM 调用记录的核心字段设计在设计 record 时建议把字段分成五组请求信息、响应信息、费用指标、链路标识、附加标记。3.1 请求信息请求信息用于回答调了什么模型、传了什么参数。字段类型说明call_idstring每次调用的唯一 ID推荐 UUIDtimestampdatetime调用发起时间建议统一存储为 UTCmodelstring模型名称例如gpt-4o-miniproviderstring服务商标识例如openai、anthropic、ollamaprompt/messagesarray / string请求内容可以是消息数组或纯文本temperaturenumber采样温度max_tokensinteger最大生成 token 数stopstring / array停止序列endpointstring实际调用的接口地址3.2 响应信息响应信息用于回答模型返回了什么、是否成功。字段类型说明responsestring / array模型返回内容status_codeintegerHTTP 状态码status_textstring错误信息或状态描述latency_msinteger调用耗时单位毫秒finish_reasonstring结束原因如stop、lengthcreated_atdatetime响应接收时间3.3 费用指标费用指标用于回答这次调用花了多少钱。字段类型说明prompt_tokensinteger输入 token 数completion_tokensinteger输出 token 数total_tokensinteger总 token 数estimated_costnumber预估成本按模型单价计算currencystring币种如USD、CNY注意token 统计优先以 API 返回的 usage 为准不要自己从头计算。预估成本需要维护一份模型单价表这部分如果没有项目内置建议放到配置文件中方便随时更新。3.4 链路标识链路标识用于回答这次调用属于哪个业务请求、哪次 Agent 执行。字段类型说明trace_idstring顶层业务请求 IDspan_idstring当前调用节点 IDconversation_idstring多轮会话 IDagent_run_idstringAgent 单次运行 IDworkflow_idstring业务流程 ID3.5 附加标记附加标记用于回答这个调用属于哪个模块、哪个用户、是否需要特殊处理。字段类型说明user_idstring用户标识脱敏后存储tagsarray自定义标签如rag、agent、testenvironmentstring环境如dev、staging、prodsampledboolean是否采样记录metadataobject业务自定义字段自由扩展4. LLM 调用 record 的环境准备与前置条件如果只是记录 LLM 调用并导出 JSONL环境准备工作非常轻。4.1 基础环境清单Python 3.9 及以上版本pydantic或标准库dataclasses用于定义 record 结构网络请求库openai、anthropic、httpx等取决于你当前使用的 SDK可选uvicornfastapi用于启动日志接收服务本地磁盘空间用于保存 JSONL 日志文件建议预留可扩展的日志目录4.2 是否需要 GPU不需要。这个方案核心是日志记录和标准化不跑推理模型。如果你同时记录本地模型的调用才需要关注模型推理侧的 GPU 资源。4.3 端口与访问如果只是作为库嵌入业务代码不占用端口。如果需要独立部署日志接收服务建议本地使用127.0.0.1:8790或按项目实际配置调整注意避免与已有服务端口冲突。# 如果通过 pip 安装项目自身依赖按实际项目名替换 pip install llm-call-record # 如果只是用轻量 HTTP 服务接收日志可安装 FastAPI 和 Uvicorn pip install fastapi uvicorn5. 定义一个 LLM 调用 record 数据结构这部分给出通用实现模板。核心思路是定义一个LLMCallRecord类所有字段使用类型标注序列化后直接写入 JSONL。from dataclasses import dataclass, field, asdict from datetime import datetime, timezone from typing import Any, Optional from uuid import uuid4 dataclass class LLMCallRecord: # 核心标识 call_id: str field(default_factorylambda: str(uuid4())) timestamp: datetime field( default_factorylambda: datetime.now(timezone.utc) ) trace_id: Optional[str] None span_id: Optional[str] None conversation_id: Optional[str] None user_id: Optional[str] None # 请求信息 provider: Optional[str] None model: Optional[str] None endpoint: Optional[str] None messages: Optional[list] None temperature: Optional[float] None max_tokens: Optional[int] None stop: Optional[list] None # 响应信息 response: Optional[Any] None status_code: Optional[int] None error_message: Optional[str] None latency_ms: Optional[int] None finish_reason: Optional[str] None # token 与费用 prompt_tokens: Optional[int] None completion_tokens: Optional[int] None total_tokens: Optional[int] None estimated_cost: Optional[float] None currency: str USD # 附加信息 environment: str dev tags: list field(default_factorylist) sampled: bool False metadata: dict field(default_factorydict) def to_json(self) - str: data asdict(self) data[timestamp] self.timestamp.isoformat() return json.dumps(data, ensure_asciiFalse)说明使用dataclass定义方便业务代码直接实例化。timestamp统一输出为 ISO 8601 格式建议存 UTC。messages和response是主数据字段内容可能较大。如果担心存储膨胀可以在写入前截断或摘要化。6. 自动捕获 LLM 调用装饰器与中间件定义好数据结构后下一步是自动捕获调用。最常使用的是 Python 装饰器。把记录逻辑和业务逻辑解耦业务函数只需要关注调用本身。import time import json import functools def record_llm_call(record: LLMCallRecord): def decorator(func): functools.wraps(func) def wrapper(*args, **kwargs): start time.perf_counter() try: result func(*args, **kwargs) record.status_code 200 record.response result # token usage 从结果中提取取决于实际 API 返回结构 usage getattr(result, usage, None) if usage: record.prompt_tokens getattr(usage, prompt_tokens, None) record.completion_tokens getattr(usage, completion_tokens, None) record.total_tokens getattr(usage, total_tokens, None) record.finish_reason getattr(result, choices, [{}])[0].get(finish_reason) return result except Exception as exc: record.status_code 500 record.error_message str(exc) write_log(record) raise finally: record.latency_ms int((time.perf_counter() - start) * 1000) if record.status_code 200: write_log(record) return wrapper return decoratorwrite_log是落盘函数示例LOG_FILE ./llm_calls.jsonl def write_log(record: LLMCallRecord): with open(LOG_FILE, a, encodingutf-8) as f: f.write(record.to_json()) f.write(\n)实际接入时只需要record_llm_call( LLMCallRecord( provideropenai, modelgpt-4o-mini, trace_idtrace_123, user_iduser_456, environmentprod, ) ) def call_llm(messages, **kwargs): # 这里替换为真实的 SDK 调用 return client.chat.completions.create( modelgpt-4o-mini, messagesmessages, **kwargs, )这种方式的优点是侵入性小业务函数主体不需要关心日志。缺点是无法覆盖超时或者 SDK 内部重试的细节如果有更细粒度的请求跟踪需求可以在 HTTP 层做拦截。7. 批量任务与日志导出单次调用记录只是第一步。实际生产环境通常需要批量导出和批量分析。7.1 JSONL 批量导出推荐使用 JSONL 而不是纯 JSON因为每行一条记录追加写入方便也方便用grep、jq等工具分析。import json from pathlib import Path def export_to_jsonl(records: list[LLMCallRecord], output_path: str ./exports/records.jsonl): Path(output_path).parent.mkdir(parentsTrue, exist_okTrue) with open(output_path, w, encodingutf-8) as f: for record in records: f.write(record.to_json() \n) def load_from_jsonl(input_path: str) - list[LLMCallRecord]: records [] with open(input_path, r, encodingutf-8) as f: for line in f: line line.strip() if line: records.append(LLMCallRecord(**json.loads(line))) return records7.2 批量任务队列设计如果 LLM 调用并发量高不建议每次调用直接同步写文件可能阻塞业务线程。通用做法是内存队列加后台批量写入。import queue import threading log_queue: queue.Queue queue.Queue(maxsize1000) def write_log_async(record: LLMCallRecord): log_queue.put(record) def batch_writer_loop(): while True: records [] for _ in range(100): try: records.append(log_queue.get_nowait()) except queue.Empty: break if records: with open(LOG_FILE, a, encodingutf-8) as f: for record in records: f.write(record.to_json() \n) time.sleep(1) threading.Thread(targetbatch_writer_loop, daemonTrue).start()这种方式可以减少磁盘写入次数降低日志 IO 对 LLM 调用延迟的影响。批量任务的关键点是队列要有上限写入失败要有重试或落盘补偿。7.3 批量任务应包含的运行参数如果一次批量任务要跑大量 LLM 调用建议每条记录增加batch_run_id方便后续按批次统计整体费用和成功率。record LLMCallRecord( provideropenai, modelgpt-4o-mini, trace_idfbatch_{batch_run_id}, tags[batch, task_name], )8. REST API 与日志服务化自用可以写 JSONL 文件但如果多个服务都需要上报 LLM 调用记录建议起一个轻量日志接收服务。8.1 启动日志接收服务示例from fastapi import FastAPI, Request import uvicorn app FastAPI() app.post(/api/llm-records) async def receive_record(request: Request): payload await request.json() record LLMCallRecord(**payload) write_log(record) return {status: ok, call_id: record.call_id} if __name__ __main__: uvicorn.run(app, host127.0.0.1, port8790)8.2 使用 curl 测试接口curl -X POST http://127.0.0.1:8790/api/llm-records \ -H Content-Type: application/json \ -d { provider: openai, model: gpt-4o-mini, messages: [{role: user, content: hello}], response: hi, status_code: 200, total_tokens: 12, latency_ms: 300 }8.3 使用 Python requests 上报import requests url http://127.0.0.1:8790/api/llm-records payload { provider: openai, model: gpt-4o-mini, trace_id: trace_001, messages: [{role: user, content: hello}], response: hi, status_code: 200, total_tokens: 12, latency_ms: 300, } resp requests.post(url, jsonpayload, timeout5) print(resp.json())注意这是通用 API 示例实际字段名和路径以项目实现为准。日志上报接口建议只监听内网地址不要随意暴露到公网。如果必须跨网络应增加鉴权字段。9. 资源占用与性能观察这块是很多人在选型时最关心的问题记录日志会影响多少性能磁盘会膨胀多少从方案本身看record 记录只做了数据结构化和文本写入不涉及模型推理性能开销相对可控。实际影响主要来自几个方面序列化开销model_dump()或asdict()对超大messages字段做深拷贝和序列化会比较耗时。如果请求里包含长文档建议在记录前做截断或只存前 N 个字符。同步 IO 阻塞每次调用都同步写文件在高并发下会阻塞业务线程。推荐使用内存队列加批量写入或者直接接入logging模块交给后台线程处理。磁盘占用一条完整记录可能几 KB 到几十 KB 不等。一天几万次调用会累积到几百 MB。建议按天分文件或者定期压缩、归档。LOG_FILE_BY_DATE f./logs/llm_calls_{datetime.now().strftime(%Y%m%d)}.jsonl存储策略可以这样安排日志类型保留时间存储方式调试日志7 天本地 JSONL成本统计90 天聚合到 SQLite 或数据库审计日志1 年以上对象存储冷备如果担心记录对主流程造成影响可以设置采样率。例如生产环境记录 100% 调用但只在debug模式下保存完整请求体平时只保存 token 和延迟指标。10. 常见问题与排查方法问题现象可能原因排查方式解决方案record 文件没有写入write_log路径不存在或权限不足检查日志文件路径创建目录并确认写权限调用上报但字段为空API 返回结构变化字段提取失败打印原始 API 返回增加字段兼容逻辑记录后磁盘暴涨完整保存了超大请求体查看单条日志大小截断长文本或只存摘要token 统计为 0未从 usage 字段解析检查 SDK 返回结构不同 provider 分别处理接口上报失败服务未启动或端口被占检查端口和日志更换端口并重启服务时间不对本地时间与 UTC 混用检查 timestamp 生成逻辑统一使用 UTC 存储展示时转本地时区线程池阻塞批量写入队列堆积观察队列长度增加消费者线程或降低采样率敏感信息泄漏未对消息内容脱敏检查存储日志内容对messages、response做规则脱敏多服务记录字段不一致没有统一使用 record 模型检查各服务依赖版本抽成公共库统一维护11. 最佳实践与合规提醒11.1 LLM 调用日志的技术最佳实践第一字段设计要一次到位。trace_id、conversation_id、user_id这类链路字段最好从第一天就加上否则后期补数据成本很高。第二记录先走 JSONL后续再上数据库。LiteLLM、Langfuse 这类产品通常自带数据库存储但自研方案从 JSONL 起步最灵活方便做数据迁移和自定义分析。第三接口服务要限制访问范围。日志记录服务只监听本机地址不要直接暴露公网。如果多个服务器上报日志用内网网段加 Token 鉴权。第四token 和成本字段要独立核算。优先从 API usage 字段取数模型单价表放到配置中心避免改代码才能调整价格。第五引入 OpenTelemetry 时保持兼容。可以在现有 record 基础上增加otel_trace_id、otel_span_id字段后续接入链路追踪平台时不需要推倒重建。11.2 隐私与合规边界涉及 LLM 调用日志时必须重视数据安全不要在日志中记录明文密码、密钥、身份证号、手机号等敏感信息。messages和response中如果包含业务数据存储前要做脱敏处理。通用做法是正则替换手机号、邮箱或者对关键字段做哈希映射。如果日志用于审计或用户行为分析需要确认数据使用范围符合相关隐私政策和用户授权协议。使用第三方 LLM API 时调用内容本身可能上送云端日志记录前应确认服务协议是否允许这种数据留存。涉及人脸、声纹、生物特征或受版权保护的素材时不应在未获得明确授权的情况下记录和使用。12. 总结与下一步这个 record 方案最值得尝试的点在于它把 LLM 调用日志从随手打几行 print升级成了结构化、可统计、可审计的工程基础能力。没有复杂的架构不依赖 GPU不强迫你切换服务商也不要求业务的调用方式做出大的调整。最先建议验证的能力是在自己的项目里接入装饰器或中间件记录一次真实的 LLM 调用检查 JSONL 输出里 token、延迟、错误信息是否完整。这一步跑通后面接批量任务、做成本统计、接入 OpenTelemetry 都很顺。最容易踩的坑有三个一是字段设计得不够开始统计费用时发现缺少conversation_id或user_id二是直接同步写日志文件高并发下拖慢接口三是没有提前做脱敏导致敏感内容进入日志系统。这三个坑都可以通过前面的设计和规范规避。下一步如果发现 JSONL 分析不方便可以给记录加上 SQLite 或 ClickHouse 存储层按天做聚合报表把模型调用次数、成功率、平均延迟、token 消耗做成看板。也可以在此基础上接入自定义的 LLM 编排框架配合 LLM Agent、RAG、MCP 工具调用场景做链路追踪逐步往成熟的 LLM 可观测体系演进。