refactor(observability): Phase2.2 成本归因 trace_id列+token日志写入+LLM/MCP/媒体子span [v2026.07.19.3]
CI / gate (push) Has been cancelled
CI / gate (push) Has been cancelled
This commit is contained in:
1 parent
6aefdfa5c2
commit
a7a57f9a03
6 files changed
+49
-22
No files matched your search
@@ -3,11 +3,20 @@
|
||||
# 版本号格式: vYYYY.MM.DD.N (年.月.日.当日递增序号,从 1 开始)
|
||||
# 环境标签: prod = 正式版 | test = 测试版
|
||||
|
||||
VERSION=v2026.07.19.2
|
||||
VERSION=v2026.07.19.3
|
||||
ENV=prod
|
||||
DATE=2026-07-19
|
||||
BASE_IMAGE=qihu-aiflow:with-deps
|
||||
BACKUP_IMAGE=qihu-aiflow:backup-20260719-002
|
||||
BACKUP_IMAGE=qihu-aiflow:backup-20260719-003
|
||||
|
||||
## 本版本变更 (v2026.07.19.3 / prod)
|
||||
- Phase2.2 成本归因:token_usage_logs 新增 trace_id 列(迁移 020_add_trace_id_token_usage_logs.sql,幂等 ALTER),打通「链路 -> 成本」归集
|
||||
- _write_media_token_log(媒体图片/视频)注入 get_trace_id(),每条媒体 TokenUsageLog 现可回溯到请求链路
|
||||
- LogService.log_token_usage 增加 trace_id 参数(为后续文本 LLM token 记账接通成本归因预留)
|
||||
- chat_service / chat_media 在 LLM 补全、MCP 工具拉取/调用、技能执行、AGNES 媒体生成处加 span 子链路(async with span),一次请求可下钻耗时分布
|
||||
- 注意:_generate_media_task 为后台 fire-and-forget,其 span 会因处于 asyncio.create_task 表达式内而非法,已移除;真实媒体耗时由 chat_media 内 media.agnes_call span 覆盖
|
||||
- 纯增量埋点,不改接口返回结构与业务行为;pytest 9 passed + vitest 4 passed 门禁绿
|
||||
- 部署前已按 SOP 备份 rollback 点 qihu-aiflow:backup-20260719-003
|
||||
|
||||
## 本版本变更 (v2026.07.19.1 / prod)
|
||||
- Phase1 chat.py 巨石拆分收官:10070 行单体拆分为 chat_{utils,media,agents,skill,mcp,context,knowledge,service}.py 共 8 个子模块,controller 层 chat.py 降至 1352 行(<3000 目标达成)
|
||||
|
||||
@@ -47,6 +47,7 @@ from ..mcp_client import list_mcp_tools, call_mcp_tool
|
||||
from ..middleware.log_service import LogService
|
||||
from ..capability_registry import resolve_auto_model
|
||||
from app.api.chat_utils import (_html_escape, _proxy_for_url, cst_now, logger)
|
||||
from app.core.observability import span, get_trace_id
|
||||
|
||||
def detect_media_intent(text: str) -> str | None:
|
||||
|
||||
@@ -958,7 +959,8 @@ async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str
|
||||
|
||||
|
||||
|
||||
resp, err = await _agnes_post_json(api_url, headers, payload, timeout)
|
||||
async with span("media.agnes_call"):
|
||||
resp, err = await _agnes_post_json(api_url, headers, payload, timeout)
|
||||
|
||||
|
||||
|
||||
@@ -1166,7 +1168,8 @@ async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str
|
||||
|
||||
|
||||
|
||||
resp, err = await _agnes_post_json(api_url, headers, payload, timeout)
|
||||
async with span("media.agnes_call"):
|
||||
resp, err = await _agnes_post_json(api_url, headers, payload, timeout)
|
||||
|
||||
|
||||
|
||||
@@ -1486,7 +1489,7 @@ async def _generate_media_task(ai_msg_id: str, ai_model_id: str, prompt: str, us
|
||||
|
||||
|
||||
|
||||
_write_media_token_log(db, ai_model, user_id, msg.conversation_id if msg else None, prompt)
|
||||
_write_media_token_log(db, ai_model, user_id, msg.conversation_id if msg else None, prompt, trace_id=get_trace_id())
|
||||
|
||||
|
||||
|
||||
@@ -1516,7 +1519,7 @@ async def _generate_media_task(ai_msg_id: str, ai_model_id: str, prompt: str, us
|
||||
|
||||
pass
|
||||
|
||||
def _write_media_token_log(db, ai_model, user_id: str, conversation_id: str, prompt: str):
|
||||
def _write_media_token_log(db, ai_model, user_id: str, conversation_id: str, prompt: str, trace_id: str = None):
|
||||
|
||||
|
||||
|
||||
@@ -1601,6 +1604,7 @@ def _write_media_token_log(db, ai_model, user_id: str, conversation_id: str, pro
|
||||
|
||||
|
||||
created_at=cst_now(),
|
||||
trace_id=trace_id,
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import httpx
|
||||
import os
|
||||
import logging
|
||||
from ..database import get_db, SessionLocal
|
||||
from app.core.observability import span
|
||||
from ..models import (
|
||||
|
||||
|
||||
@@ -791,7 +792,8 @@ async def _send_message_core(
|
||||
|
||||
try:
|
||||
|
||||
_tools = await list_mcp_tools(_srv)
|
||||
async with span("mcp.list_tools", server=_srv.get("name") if isinstance(_srv, dict) else None):
|
||||
_tools = await list_mcp_tools(_srv)
|
||||
|
||||
except Exception as _e:
|
||||
|
||||
@@ -1192,35 +1194,36 @@ async def _send_message_core(
|
||||
|
||||
|
||||
|
||||
resp = await client.post(
|
||||
async with span("llm.completion", model=ai_model.id if ai_model else None):
|
||||
resp = await client.post(
|
||||
|
||||
|
||||
|
||||
api_url,
|
||||
api_url,
|
||||
|
||||
|
||||
|
||||
headers={
|
||||
headers={
|
||||
|
||||
|
||||
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
|
||||
|
||||
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json"
|
||||
|
||||
|
||||
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
|
||||
json=request_body
|
||||
json=request_body
|
||||
|
||||
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -1380,21 +1383,23 @@ async def _send_message_core(
|
||||
|
||||
if _srv:
|
||||
|
||||
tool_result = await call_mcp_tool(
|
||||
async with span("mcp.call", name=_info.get("name") if isinstance(_info, dict) else None):
|
||||
tool_result = await call_mcp_tool(
|
||||
|
||||
_srv, _info["name"], func_args
|
||||
_srv, _info["name"], func_args
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
called_mcp_names.append(_srv.get("name", ""))
|
||||
|
||||
else:
|
||||
|
||||
tool_result = _execute_skill_tool(
|
||||
async with span("skill.execute", name=func_name):
|
||||
tool_result = _execute_skill_tool(
|
||||
|
||||
db, func_name, func_args, skills, req.content
|
||||
db, func_name, func_args, skills, req.content
|
||||
|
||||
)
|
||||
)
|
||||
|
||||
_sk = next((s for s in skills if s.get("func_name") == func_name), None)
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ class LogService:
|
||||
app_id: str = None, app_name: str = None,
|
||||
conversation_id: str = None, prompt_tokens: int = 0,
|
||||
completion_tokens: int = 0, total_tokens: int = 0,
|
||||
cost: float = 0):
|
||||
cost: float = 0, trace_id: str = None):
|
||||
"""记录Token消耗日志"""
|
||||
db = SessionLocal()
|
||||
try:
|
||||
@@ -73,6 +73,7 @@ class LogService:
|
||||
completion_tokens=completion_tokens,
|
||||
total_tokens=total_tokens,
|
||||
cost=cost,
|
||||
trace_id=trace_id,
|
||||
)
|
||||
db.add(log)
|
||||
db.commit()
|
||||
|
||||
@@ -537,6 +537,7 @@ class TokenUsageLog(Base):
|
||||
app_id = Column(String(36), default=None)
|
||||
app_name = Column(String(200), default=None)
|
||||
conversation_id = Column(String(36), default=None)
|
||||
trace_id = Column(String(64), default=None)
|
||||
prompt_tokens = Column(Integer, default=0)
|
||||
completion_tokens = Column(Integer, default=0)
|
||||
total_tokens = Column(Integer, default=0)
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
-- Phase 2.2: add trace_id to token_usage_logs for cost attribution (idempotent)
|
||||
SET @db = DATABASE();
|
||||
SET @col_exists = (SELECT COUNT(*) FROM information_schema.columns WHERE table_schema = @db AND table_name = 'token_usage_logs' AND column_name = 'trace_id');
|
||||
SET @sql = IF(@col_exists = 0, 'ALTER TABLE token_usage_logs ADD COLUMN trace_id VARCHAR(64) NULL DEFAULT NULL', 'SELECT 1');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
Reference in new issue
Block a user