algo_log · Postgres
算法服务把一次任务打进 Logfire。抽取程序按固定规则把它落进 Postgres schema algo_log,用来回放、对账、做训练集。
列名和来源由代码决定,和库里现在有没有数据无关。
抽取先找 Logfire 里名字以 workflow: 开头的根记录,再按同一个 trace_id 拉齐步骤、模型调用和 HTTP。
检视页里的图走 GET /api/media,现场签深圳 CDN,有效期 7 小时,不用为了看图再抽一次。
一行对应一次任务。幂等键是 source + logfire_trace_id + logfire_span_id。会话级 distributed tracing 会让多次 workflow 共用一个 trace,不能只按 trace 覆盖。
| 字段 | 类型 | 从哪读 |
|---|---|---|
id | varchar(26) | ETL 生成 ULID,不是日志里的 |
logfire_trace_id | text | 根 span 的 trace_id |
logfire_span_id | text | 根 span 的 span_id |
workflow_name | text | 根 span 的 span_name,去掉前缀 workflow:。outfit recommend 为 outfit_recommend_submit_task |
task_id | text | 根 span attributes.task_id |
user_id | text | 根 span attributes.user_id |
started_at | timestamptz | 根 span start_timestamp |
duration_ms | bigint | 根 span duration × 1000 |
status | text | 根 span is_exception → failed,否则 success |
source | text | Logfire 项目:mirror;server 里 service_name=algorithm → server-aws,其余 → server-shenzhen |
params | jsonb | 根 span attributes.workflow_params(没有则用其余 attributes) |
一行对应 Logfire 里名为 step:… 的一步。只有父记录也是 step 时才填 parent_step_id;父是整次任务则为空。
| 字段 | 类型 | 从哪读 |
|---|---|---|
id | varchar(26) | ETL 生成 ULID |
run_id | varchar(26) | 所属 workflow_run.id |
parent_step_id | varchar(26) | 父 span 若也是 step:*,指向那行 id;父是 workflow 则为空 |
logfire_span_id | text | 该 step span 的 span_id |
seq | int | 同一 run 内按 start_timestamp 排序后的序号,从 1 起 |
step_name | text | 优先 attributes.step,否则 span_name 去掉 step: |
started_at | timestamptz | 该 step span 的 start_timestamp |
duration_ms | bigint | 该 step span 的 duration × 1000 |
status | text | 该 span is_exception |
input | jsonb | step span attributes,去掉 step、task_id。常见键:s3_path、query、image_url、raw_query、outfit_index |
output | jsonb | 挂在该 step 下的日志 span_name = step_result 的 attributes.result(run_timed_step 打的) |
timings | jsonb | 该 step 下 GET/POST/PUT、yolo.* 子 span 的耗时(网络 vs 本地) |
events | jsonb | 可回放子事件:*.skipped、*.knn_hits、*.input / *.result;丢掉 embedding 向量 |
一行对应一次模型调用。优先读业务日志 *.response;prompt / messages 来自同一次 POST …/chat/completions 的 http.request.body.text,完整回复在子 span Reading response body 的 http.response.body.text。没有 HTTP 体时再看 *.input.context_messages。检视页单独开 trace 区块,按 context_messages 展示 system / human / images / assistant。生图输出是图 URL,不是字节。base64 图换成 URL 或 [image],密钥丢掉。
| 字段 | 类型 | 从哪读 |
|---|---|---|
id | varchar(26) | ETL 生成 ULID |
run_id | varchar(26) | 所属 run |
step_id | varchar(26) | 最近父 step 的 id;找不到则空 |
logfire_span_id | text | 该日志的 span_id |
seq | int | 同一 run 内按时间排序 |
agent_name | text | span_name 去掉后缀 .response,例如 outfit_recommender |
started_at | timestamptz | 该日志 start_timestamp |
output_text | text | attributes 里第一个有值的 response / content / output |
output_json | jsonb | 上面那个值若是 object/array,或能 json.loads 的字符串 |
prompt_text | text | 兼容旧检视:messages 里的全文拼在一起,图写成 [image],不截断 |
context_messages | jsonb | 清洗后的 OpenAI messages:system / user / assistant 按调用顺序。优先 *.input;没有则从 POST 投影全文,不截断、不拉图片 base64 |
request_params | jsonb | httpx 请求体去掉 messages 之后的全部参数:url、model、temperature、enable_thinking、response_format 等;extra_body 会摊平 |
token_usage | jsonb | 优先 Reading response body 的 usage:model / prompt_tokens / completion_tokens / cached_tokens。新抽取不再拉 llm.token_usage |
duration_ms | bigint | 这次调用耗时。优先 HTTP span duration,没有则用 *.response 上的 ms 或父 span |
入口 services/workflow_outfit_recommend_submit_task.py。
根日志名叫 workflow:outfit_recommend_submit_task。
有没有 query 理解、扩写、试衣,取决于这次请求带了什么。
| step_name | 何时有这一行 | workflow_step.input 读自 | workflow_step.output 读自 | llm_call |
|---|---|---|---|---|
body_model |
每次都有 | span step:body_model 的 count |
该 step 下 step_result.result(人像列表) |
无 |
recommend_query_understand |
有 raw_query 且 layout 带锚点单品 |
span 的 raw_query |
step_result.result(解析后的 outfits) |
有则写:recommend_query_understand.input + .response |
recommend_query_expand |
enable_query_expand=true |
span 的 outfit_count |
step_result.result |
有则写:recommend_query_expand.input + .response |
outfit_thinking |
当前代码会跳过,一般没有这一行 | — | — | outfit_thinker.input + .response(当前代码常跳过) |
outfit_recommend |
每次都有(后台 task,仍挂在同一 trace) | span 的 outfit_count、trigger 等 |
step 成功时的业务输出(outfits) | 核心:outfit_recommender.input + outfit_recommender.response,每套 / 每次 retry 一条 |
outfit_reason |
每套有衣服时一条 | span 的 outfit_index |
step_result.result(reason 文案) |
outfit_reason.response;prompt 在 outfit_reason.input |
tryon_tips |
准备试衣前一条 | span 的 outfit_index |
step_result.result(穿着说明) |
tryon_tips.response;prompt 与衣图 URL 在 tryon_tips.input |
vton_image |
每套能渲染时一条 | body_model_id、outfit_index |
step_result.result(含 image_url) |
vton_image.input + vton_image.response(prompt / 输入图 / 输出图 URL) |
outfit_check |
vton 出图后一条 | span 的 image_url |
step_result.result |
outfit_check.response;prompt 在 outfit_check.input |
outfit_scene_describer |
vton 出图后一条 | span 的 image_url |
step_result.result(一级场景) |
outfit_scene_describer.response;prompt 在 *.input |
v1 / v2 步骤几乎相同,检索那一步分别叫 closet_search 和 closet_search_v2。
| step_name | workflow_step | llm_call |
|---|---|---|
search_query_understand |
有 raw_query 时。input.raw_query;output = 解析后的 query/category |
search_query_understand.input + .response;展示标签是 search_display_keywords.input/.response |
closet_search 或 closet_search_v2 |
每次都有。input.query;output = 检索结果 |
无 |
search_display_keywords 是后台另起的任务,不一定和这次检索在同一条 trace,抽不到是预期行为。
入口 services/workflow_ootd_v2_submit_task.py。人像分割、描边识别、合并、去重、描述会按件展开。
| step_name | workflow_step.input / output | llm_call |
|---|---|---|
segment_person | input.s3_path;output 分割结果 | 无 |
outfit_describe | input.s3_path;output 穿搭描述 | outfit_describer.input + .response |
separate_person / human_parse | CV 结果在 output | 无 |
cloth_outline_name_describe 等 | 输入图 + 识别文本;描边图算输入 | 每轮 cloth_outline_multi_describe.{turn}.input/.response,以及 Reading response body / chat POST 标量 |
cloth_items_merge | 输入候选图;输出单品名 | 类别冲突时 cloth_items_merge.input/.response |
cloth_embedding_deduplicate | 查询图 + 候选图;output 为判定结果 | cloth_embedding_dedup_verify.input/.response |
cloth_regenerate / cloth_describe / cloth_soul / cloth_brand_describe | 按件;input 含图 URL | 对应 *.input + *.response |
outfit_detail_describe | output 细节描述 | outfit_detail_describer.input + .response |
generate_cover_image / generate_explosion_image | 输入方图 URL;输出海报 URL | *.input + *.response(prompt / 输入图 / 输出图 URL,不是字节) |