AI Agent 上线前要补的配置、日志与成本账
把 Agent 从 demo 推到能给别人用,中间隔的不是模型能力,是三个很土的东西:配置、日志、成本。这三样在 demo 阶段都能糊过去,上线之后每一样都会单独咬你一口。
这篇不聊架构选型,就写这三件最小该做到什么程度。
配置要收成一份
Agent 的配置比普通服务多一类东西:它同时有"部署配置"和"行为配置"。部署配置是 API 地址、并发数、超时;行为配置是模型选哪个、温度多少、工具开哪些、系统提示词用哪版、最大步数。前者按环境变,后者按实验变,混在一起就会出现"想让测试环境换个模型,结果把生产的并发也改了"。
判断标准很简单:把配置分成两层,一层随环境走(env),一层随会话走(请求参数或数据库里的一行记录)。行为配置不要写进环境变量,因为它需要按任务、按用户灰度,而环境变量是进程级的,改了要重启。
from dataclasses import dataclass, field
@dataclass(frozen=True)
class DeployConfig:
base_url: str
api_timeout: float = 20.0
max_concurrency: int = 4
@dataclass(frozen=True)
class AgentProfile:
name: str
model: str
temperature: float
max_steps: int
tools: tuple[str, ...] = field(default_factory=tuple)
prompt_version: str = "v1"
`AgentProfile` 的每个字段都应该能单独改、单独回滚,并且带版本号。`max_steps` 尤其要有,它是防失控的最后一道闸:模型陷入"调工具 → 看结果 → 再调同一个工具"的循环时,唯一能停下来的是步数上限,不是提示词。
工具开关也要显式。默认全开是非常危险的设计——新接一个能发邮件的工具,所有历史任务都白得了一个发邮件的权限。给工具的默认值是空元组,要什么显式加什么。
日志要能串成一条链
普通服务的日志靠 request_id 串。Agent 不够,因为它有嵌套:一次用户请求里可能有多轮模型调用,每轮里有多个工具调用,工具背后可能还有子 Agent。至少要三级 id:任务级、轮次级、调用级。
import json
import time
def log_event(task_id: str, turn: int, event: str, **fields):
record = {
"ts": round(time.time(), 3),
"task_id": task_id,
"turn": turn,
"event": event,
**fields,
}
print(json.dumps(record, ensure_ascii=False), flush=True)
三种事件必须有,缺一种都会在出事时抓瞎:
`llm_call`,记模型名、prompt 版本、输入 token、输出 token、耗时、finish_reason。`finish_reason` 经常被忽略,但它是判断"模型自己停下来"还是"被 max_tokens 截断"的唯一依据,截断导致的工具调用参数不完整,事后只能靠这个字段回溯。
`tool_call`,记工具名、参数摘要、耗时、成败、错误分类。参数要摘要不要全量,一是日志体积,二是参数里常有用户隐私。摘要方式建议保留键名和值类型,值只留长度或哈希。
`turn_end`,记这一轮的累计 token、累计耗时、是否命中步数上限。有这条,才算得出"一个任务到底花了多少"。
还有一条实践:**把模型原始输出也存一份**。不是存日志,是存到对象存储,日志里只留引用。原因很实在,模型返回的 JSON 解析失败时,你需要看原文;但原文动辄几千字符,塞进日志会把日志系统压垮。
成本要按任务算
按月看账单没用,你只知道贵,不知道贵在哪。成本必须按三个维度切开:按任务、按工具、按任务类型。
按任务算是为了找出异常值。一个每天跑两百次的任务突然某天花了平时五倍,通常是循环或者上下文膨胀,这时候需要单任务的成本能一眼看到。
按工具算是为了找出隐形大户。很多工具返回的内容很长(网页正文、搜索结果、数据库行),这些内容进上下文会按输入 token 反复计费——一轮进一次,五轮就是五次。一个返回两万字的工具,可能比模型本身还贵。
按任务类型算是为了做取舍。同样是问一句话,走"直接回答"和走"检索 + 重排 + 生成"差十几倍。数据摆出来,才知道哪些任务值得上重流程。
PRICE = {"input": 2.0, "output": 10.0} # 美元 / 百万 token
def cost_of(usage: dict) -> float:
return (usage.get("prompt_tokens", 0) * PRICE["input"]
+ usage.get("completion_tokens", 0) * PRICE["output"]) / 1_000_000
def task_cost(events: list[dict]) -> dict:
total = sum(cost_of(e.get("usage", {})) for e in events if e["event"] == "llm_call")
by_tool = {}
for e in events:
if e["event"] == "tool_call":
by_tool[e["tool"]] = by_tool.get(e["tool"], 0) + e.get("context_tokens", 0)
return {"cost_usd": round(total, 4), "tool_context_tokens": by_tool}
`context_tokens` 要由工具层自己报,不是模型报。工具返回了多长的内容、这些内容进了几轮上下文,只有工具层清楚。少了这个字段,成本只能算个总数,拆不开。
三个容易忽略的细节
第一,超时和步数上限要能区分"模型笨"和"系统慢"。日志里如果只有"任务失败",你没法判断该改提示词还是该扩并发。把 `finish_reason`、`max_steps_reached`、`tool_timeout_count` 三个计数分开记,一天的日志就能告诉你是哪一类。
第二,别在日志里存完整系统提示词。提示词改版很频繁,每行日志都带一份,日志体积会翻几倍。存版本号,提示词正文单独存一份,按版本索引。
第三,重放能力要从第一天就有。线上出一个坏 case,最省时间的排查方式是拿当时的输入、当时的配置版本、当时的工具返回,在本地重跑一遍。这要求日志里的输入是可重建的,工具返回有留存。等出事了再想补,历史数据已经没了。
做到什么程度算够
不需要一上来就搭完整的可观测平台。最低配是:配置分层且带版本、日志有三类结构化事件且能按任务串起来、成本能按任务和工具拆开。这三样加起来大概两三百行代码,但缺一样,后面的问题排查就会从"看日志"退化成"猜"。
真正会拖垮项目的,从来不是模型不够强,是出了问题以后没人能在一小时内说清发生了什么。
更多推荐



所有评论(0)