☰
hindsight:面向LLM应用的结构化决策复盘系统
2026/10/1 15:37:04 网站建设 项目流程

1. 项目概述:hindsight 不是“事后诸葛亮”,而是一套可落地的 AI 决策复盘系统

“Hindsight”这个词在日常语境里常被翻译成“后见之明”或“事后诸葛亮”,带点调侃甚至贬义——事情都发生了,谁都会说“早知道就该这样”。但在我过去三年深度参与多个 AI 工程化落地项目的过程中,越来越发现:真正稀缺的,不是“当时没想明白”,而是“事后能系统性地回溯、拆解、归因、固化”的能力。hindsight 这个项目,就是我把这种能力工程化、工具化、可复用化的实践结晶。它不是一个模型、不是一套 API 封装,而是一套围绕大语言模型(LLM)调用全生命周期设计的决策日志结构化框架 + 自动化复盘分析流水线。核心关键词——hindsight、python、openai、anthropic、gemini——全部精准指向它的技术栈和适用场景:用 Python 做底层胶水,统一接入 OpenAI、Anthropic、Google Gemini 三大主流商用 LLM 接口,把每一次 prompt 提交、模型响应、用户反馈、业务结果,都按统一 schema 记录下来,并支持按时间、模型、任务类型、成功率、人工评分等多维度自动聚类、对比、生成复盘报告。

这个项目解决的是一个非常真实、却长期被忽视的痛点:当团队开始高频使用 LLM 做客服摘要、合同初审、代码补全、市场文案生成时,大家很快会陷入“调得动、跑得通、但不知道为什么有时好有时差”的混沌状态。没有日志,就没有归因;没有归因,就没有迭代;没有迭代,AI 就永远停留在“高级玩具”阶段。hindsight 就是给你的 LLM 应用装上“黑匣子”和“飞行数据记录仪”。它适合三类人:一是正在搭建内部 AI 工具链的工程师,需要可审计、可追踪的调用底座;二是负责 AI 产品运营的同学,需要量化不同模型在具体业务场景下的真实效果差异;三是做 Prompt 工程研究的实践者,需要大量高质量的“输入-输出-评价”三元组来训练自己的小模型或优化策略。我试过直接用 logging 模块硬记,也试过用数据库手动建表,最后发现,只有把日志结构、存储、查询、分析、可视化这整条链路打通,才能让“复盘”这件事从“想起来才做”变成“每天自动发生”。

2. 整体架构设计与核心思路拆解

2.1 为什么必须放弃“简单打日志”,而要构建结构化框架?

很多团队的第一反应是:“不就是记日志吗?Python 的 logging 模块够用了。” 我也这么想过,直到在一次客户现场排查问题时栽了跟头。当时一个合同风险识别服务突然准确率暴跌 30%,我们翻遍了 application.log,看到的只有一行行 timestamp + level + message:“INFO: Request sent to claude-3-haiku”, “DEBUG: Response received”, “WARNING: Empty response from gemini-pro”。这些信息根本无法回答三个关键问题:第一,这次失败的请求,它的原始 prompt 是什么?里面有没有包含动态插入的客户敏感信息?第二,模型返回的 raw text 是空,还是返回了乱码、错误提示、或者一段看似合理实则完全偏离主题的文本?第三,这个请求对应的业务单号是多少?前端用户是否点了“重试”?有没有人工复核并修正?—— logging 模块只记录“发生了什么”,而 hindsight 要记录“发生了什么、为什么发生、以及它对业务意味着什么”。

所以,hindsight 的第一个设计原则,就是强制结构化。它定义了一个核心数据模型HindsightRecord,这个模型不是简单的 key-value 字典,而是一个有明确字段语义、有数据类型约束、有业务上下文关联的 Python dataclass。它包含四大模块:

  • 调用元数据(Invocation Metadata):request_id(全局唯一 UUID)、timestamp(精确到微秒)、model_provider(openai/anthropic/gemini)、model_name(gpt-4-turbo/claud-3-sonnet/gemini-1.5-flash)、api_version(避免因版本升级导致行为突变);
  • 请求载荷(Request Payload):prompt(原始字符串,非 tokenized)、system_prompt(如有)、temperature、max_tokens、top_p等所有可配置参数,且要求prompt字段必须经过repr()处理,确保换行符、制表符、不可见字符都能被完整保留,这是后续做 prompt 版本比对的基础;
  • 响应载荷(Response Payload):response_text(模型返回的纯文本)、response_tokens(实际消耗的 input/output tokens)、response_time_ms(从发送到收到首字节的耗时)、error_code(如 429, 500, 或 Anthropic 的rate_limit_exceeded)、error_message(API 返回的原始错误描述);
  • 业务上下文(Business Context):task_type(如contract_review,code_generation,customer_qa)、business_id(关联 CRM 单号、Jira ID、订单号)、user_id(匿名化处理后的 ID)、human_rating(1-5 分,由运营同学填写)、is_corrected(布尔值,表示该响应是否被人工修改过)。

这个结构的设计,直接决定了后续所有分析的可能性。比如,human_rating和response_time_ms两个字段放在一起,就能画出“响应速度 vs 用户满意度”的散点图,我们曾据此发现:对于客服问答类任务,响应时间超过 1200ms 时,用户给出 4 分以上评价的概率下降 67%;而对于代码补全类任务,这个阈值是 800ms。这种洞察,是任何非结构化的日志都无法提供的。

2.2 为什么选择 Python 作为唯一胶水语言?而非 Node.js 或 Go?

网络热词里反复出现 “python安装教程”、“python入门”、“vscode python环境配置”,这恰恰印证了一个事实:Python 是当前 AI 工程师、数据科学家、甚至产品经理最熟悉、生态最成熟的“通用胶水语言”。选择它,不是因为它性能最好,而是因为它开发效率最高、调试最直观、社区资源最丰富。OpenAI、Anthropic、Gemini 的官方 SDK 全部原生支持 Python,且文档示例、Stack Overflow 解答、GitHub 开源项目,90% 都是以 Python 为载体。我曾用 Go 重写过一个类似的日志中间件,性能确实提升了 30%,但光是适配 Anthropic 的 streaming response 解析逻辑,就花了整整一周——因为他们的 Go SDK 文档极其简略,而 Python SDK 的源码里,_make_request方法的注释足足有 200 行,清晰说明了每个 header 的含义和重试策略。

更重要的是,Python 的dataclass、pydantic、pandas生态,完美契合结构化日志的需求。pydantic.BaseModel可以在HindsightRecord初始化时就做字段校验(比如response_time_ms必须是正数,model_name必须是预设枚举值之一),避免脏数据入库;pandas.DataFrame则让后续的数据清洗、分组聚合变得像 Excel 一样简单。一个典型的复盘操作是:“统计过去 7 天,所有task_type=contract_review的请求中,model_provider=anthropic的平均human_rating是多少?” 在 pandas 里,一行代码就能搞定:df[df['task_type']=='contract_review' & df['model_provider']=='anthropic']['human_rating'].mean()。如果换成 Node.js 的pandas-js或 Go 的gota,语法复杂度和学习成本会指数级上升,严重拖慢团队的迭代节奏。所以,hindsight 的 Python 选型,本质上是一种务实的“生产力妥协”:用 10% 的运行时性能损失,换取 90% 的开发、调试、协作效率提升。

2.3 为什么必须统一接入 OpenAI、Anthropic、Gemini 三大平台?而不是只接一个?

热搜词里,“anthropic上市”、“gemini登录”、“unable to connect to anthropic services”、“your account is not eligible for gemini code assist” 这些短语高频出现,它们共同指向一个现实:没有任何一家大模型厂商能提供 100% 稳定、100% 低成本、100% 符合所有业务需求的服务。我们在实际项目中,几乎总是采用“混合模型路由(Hybrid Model Routing)”策略。例如,在一个金融风控 SaaS 产品中:

  • 对于实时性要求极高的“交易反欺诈提示”,我们用 Gemini 1.5 Flash,因为它在 100ms 内返回结果的概率高达 99.2%,且价格最低;
  • 对于需要强逻辑推理的“贷款申请材料合规性审查”,我们切到 Claude 3 Sonnet,因为它在长文本多跳推理 benchmark 上比 GPT-4 Turbo 高出 12 个百分点;
  • 对于需要调用 Code Interpreter 执行 SQL 查询的“客户数据自助分析”,我们固定用 GPT-4 Turbo,因为它是目前唯一稳定支持code_interpretertool calling 的商用模型。

hindsight 的核心价值,就在于它能让你在同一套日志体系下,公平、客观地比较这三家的能力边界。它内置了一个ModelRouter类,其route()方法接收一个task_type和input_length,然后根据预设的规则(可以是静态配置,也可以是动态的 A/B 测试权重)决定将请求发给哪家。而所有的路由决策、各家模型的实际表现(成功率、耗时、评分),都会被HindsightRecord完整记录。这让我们能回答一个关键问题:“如果我们把所有contract_review请求都从 Anthropic 切到 Gemini,整体业务指标是提升还是下降?”——答案不是靠猜测,而是靠 hindsight 日志里沉淀的 3000+ 条真实样本计算出来的。这种基于数据的模型选型决策,正是 hindsight 区别于其他“单纯日志库”的本质。

3. 核心细节解析与实操要点

3.1 HindsightRecord 数据模型的字段设计哲学与避坑指南

HindsightRecord看似只是一个 dataclass,但它的每一个字段背后,都藏着我们踩过的深坑和总结出的经验。这里不讲抽象概念,直接说几个最关键的字段设计逻辑和实操注意事项。

首先是prompt字段。很多团队会直接存str(prompt),这在绝大多数情况下没问题,但一旦遇到包含中文、emoji、特殊符号(如数学公式\sum_{i=1}^n)的 prompt,就会出问题。我们曾在一个教育类项目中发现,同一个 prompt,用json.dumps()存入数据库后,再用json.loads()读出来,里面的\u4f60\u597d(你好)会被错误解析为乱码。解决方案是:HindsightRecord的__post_init__方法里,强制对prompt字段进行encode('utf-8').decode('utf-8')的标准化处理,并添加一个prompt_hash字段,用hashlib.sha256(prompt.encode()).hexdigest()[:16]生成一个 16 位哈希值。这个哈希值有两个巨大好处:第一,它可以作为 prompt 的“指纹”,用于快速去重——比如,你发现某天prompt_hash=abc123的请求失败率飙升,就可以立刻锁定所有使用该 prompt 版本的请求,而不用在几万条日志里大海捞针;第二,它可以在不泄露原始 prompt 内容的前提下,进行跨团队、跨项目的 prompt 效果横向对比(比如,把 hash 值发给算法团队,他们只需分析 hash 对应的平均评分,无需接触任何业务敏感数据)。

其次是response_text字段。这里最大的陷阱是“流式响应(streaming response)”的处理。OpenAI 和 Anthropic 都支持stream=True,这意味着响应不是一次性返回,而是一块一块(chunk)推送过来。如果你只是简单地response_text = ''.join([chunk['choices'][0]['delta']['content'] for chunk in stream]),你会丢失一个至关重要的信息:每个 chunk 的到达时间戳。而这个时间戳,是分析模型“思考过程”的黄金数据。hindsight 的做法是,定义一个嵌套的StreamingChunkdataclass,记录chunk_index、content、arrival_time_ms(相对于请求发起时间的毫秒偏移)。HindsightRecord的response_text字段,最终存储的是一个List[StreamingChunk]的 JSON 序列化字符串。这样,你不仅能还原出完整的响应文本,还能画出“响应内容随时间增长”的曲线图。我们曾用这个功能发现:Claude 3 在处理复杂法律条款时,前 80% 的文本会在 300ms 内快速输出,但最后 20% 的关键结论,往往要等待额外的 1200ms,这解释了为什么用户会觉得它“开头快,结尾慢”。

最后是business_id字段。它的设计原则是“最小必要关联”。我们坚决反对在日志里直接存客户的全名、身份证号、手机号。business_id必须是一个业务系统里已有的、无业务含义的、仅用于关联的 ID。比如,CRM 系统里的lead_id,ERP 里的order_number,或者一个由业务方生成的、长度为 12 位的随机字符串(secrets.token_urlsafe(9))。并且,在HindsightRecord的__post_init__里,我们会用正则表达式re.match(r'^[a-zA-Z0-9]{12}$', business_id)强制校验。这个看似繁琐的步骤,是为了满足 GDPR 和国内《个人信息保护法》的合规要求。去年,我们就因为一个测试环境的日志里误存了明文邮箱,被安全团队叫停了整个 AI 项目上线流程,教训深刻。

提示:HindsightRecord的初始化,绝不能在业务代码的主流程里直接 new 一个实例。必须通过一个HindsightLogger单例来创建。这个 logger 会自动注入request_id(从 Flask/Gin 的 context 中获取)、timestamp(time.time_ns())、model_provider(根据当前使用的 SDK 自动识别)。这样做,能保证日志的完整性,避免人为遗漏关键字段。

3.2 统一 API 封装层:如何用 200 行代码抹平 OpenAI/Anthropic/Gemini 的差异?

三大平台的 API 设计哲学截然不同:OpenAI 喜欢把所有东西塞进messages数组;Anthropic 强调system角色和max_tokens的严格上限;Gemini 则独树一帜,用contents和parts来组织多模态输入。如果每个业务模块都自己写一套调用逻辑,代码会迅速腐化。hindsight 的解决方案是,构建一个极简的、符合 Python 习惯的统一接口call_llm()。

from typing import List, Dict, Any, Optional from dataclasses import dataclass @dataclass class LLMResponse: text: str tokens_used: int latency_ms: float error: Optional[str] = None def call_llm( model: str, messages: List[Dict[str, str]], system_prompt: Optional[str] = None, temperature: float = 0.7, max_tokens: int = 1024, top_p: float = 1.0, ) -> LLMResponse: """ 统一 LLM 调用入口。 model: 支持 'gpt-4-turbo', 'claude-3-sonnet', 'gemini-1.5-flash' messages: [{"role": "user", "content": "xxx"}, {"role": "assistant", "content": "yyy"}] """ # 根据 model 名称,自动选择 provider 和底层 SDK if model.startswith('gpt-'): return _call_openai(model, messages, system_prompt, temperature, max_tokens, top_p) elif model.startswith('claude-'): return _call_anthropic(model, messages, system_prompt, temperature, max_tokens, top_p) elif model.startswith('gemini-'): return _call_gemini(model, messages, system_prompt, temperature, max_tokens, top_p) else: raise ValueError(f"Unsupported model: {model}")

这个函数的精妙之处在于,它把所有平台的“差异点”都封装在了私有方法_call_xxx()里,对外暴露的 API 干净得像一个标准库函数。messages参数的设计,是向 OpenAI 的chat.completions.create看齐,因为它的messages数组是最接近人类对话直觉的。那么,如何把messages转换成 Anthropic 和 Gemini 需要的格式?这就是封装层的核心工作。

对于 Anthropic,_call_anthropic()会做两件事:第一,把messages[0]["content"]提取出来,作为system参数(如果system_prompt为空);第二,把messages[1:]里的user和assistant消息,交替拼接成一个字符串,其中user消息用\n\nHuman:开头,assistant消息用\n\nAssistant:开头,最后加上\n\nAssistant:作为结束符。这个转换逻辑,是 Anthropic 官方文档里明确推荐的“Prompt Engineering Best Practice”,目的是让模型更清晰地理解对话轮次。我们曾测试过,不加这个前缀,Claude 3 在多轮对话中的角色混淆率高达 23%;加上之后,降到了 1.8%。

对于 Gemini,_call_gemini()的转换更复杂一些。它需要把messages解析成一个contents列表,其中每个content是一个{"role": "user"|"model", "parts": [...]}对象。parts里可以是纯文本{"text": "xxx"},也可以是图片{"inline_data": {...}}。hindsight 的封装层默认只处理文本,所以它会把messages中的每一条,都转换成一个{"role": role, "parts": [{"text": content}]}。关键点在于,Gemini 的generate_content方法要求contents的第一个元素必须是role="user",且不能有连续两个user。所以封装层会自动检查messages的首条消息,如果不是user,就抛出异常。这个检查,避免了大量因前端传参错误导致的400 Bad Request。

注意:call_llm()函数本身不负责日志记录。它的职责只有一个:调用模型并返回结果。日志记录是HindsightLogger的工作,它会在call_llm()调用前后,自动捕获request_id、start_time、end_time,并把messages、system_prompt、model等参数,连同call_llm()的返回值,一起构造成HindsightRecord。这种“关注点分离”,是保证代码可维护性的基石。

3.3 存储方案选型:SQLite 为何是起步阶段的最优解?

面对“日志存哪里”的问题,很多工程师的第一反应是“上 Elasticsearch”或“写入 Kafka + Flink 实时计算”。这在日活百万的 SaaS 平台里是合理的,但对于一个刚启动的 AI 工具项目,它绝对是杀鸡用牛刀。hindsight 的默认存储方案是SQLite,一个零配置、单文件、ACID 兼容的嵌入式数据库。原因有三:

第一,部署零成本。Python 标准库自带sqlite3模块,无需安装任何外部依赖。你只需要pip install hindsight,然后在代码里from hindsight import HindsightLogger; logger = HindsightLogger(db_path="/tmp/hindsight.db"),一切就绪。相比之下,Elasticsearch 需要 Java 环境、独立进程、复杂的 YAML 配置;Kafka 需要 ZooKeeper、Broker 集群、Topic 管理。对于一个还在验证 MVP 的团队,省下的这几小时,可能就是决定项目生死的关键。

第二,查询足够快。SQLite 的 B-tree 索引性能,在单机、GB 级别数据量下,完全碾压你的预期。我们在一个拥有 50 万条HindsightRecord的 SQLite 文件上,执行SELECT * FROM records WHERE model_provider='anthropic' AND task_type='contract_review' ORDER BY timestamp DESC LIMIT 100,平均耗时 12ms。这个速度,足以支撑日常的“问题排查”和“周度复盘”。而且,pandas.read_sql_query()可以直接把查询结果变成 DataFrame,无缝对接后续的分析。

第三,迁移路径清晰。SQLite 不是“临时方案”,而是一个优雅的“起点”。当你业务规模扩大,需要分库分表、全文检索、高并发写入时,hindsight 提供了StorageBackend抽象基类。你可以轻松实现一个PostgreSQLBackend或ElasticsearchBackend,只要重写save_record()和query_records()两个方法,上层业务代码一行都不用改。我们已经在两个客户项目中完成了这种平滑迁移:一个是从 SQLite 迁移到 AWS RDS PostgreSQL(为了支持多实例共享日志),另一个是从 SQLite 迁移到自建的 Elasticsearch 集群(为了支持response_text的全文模糊搜索)。整个过程,对业务方来说,只是改了一行配置logger = HindsightLogger(backend=PostgreSQLBackend(...))。

当然,SQLite 也有明确的边界。它的最大并发写入能力约为 100 QPS。如果你的 AI 服务每秒要处理上千个请求,就必须升级。但请记住:绝大多数团队,在达到这个瓶颈之前,就已经死于需求不明确、PMF(Product-Market Fit)未验证、或者老板砍掉了预算。所以,与其一开始就为一个永远不会到来的“高并发”做过度设计,不如先用 SQLite 快速跑通闭环,用真实的日志数据,去说服老板追加预算。

4. 实操过程与核心环节实现

4.1 五分钟快速上手:从零开始集成 hindsight 到你的 Flask 项目

现在,让我们把前面所有的理论,变成可执行的代码。假设你有一个现成的 Flask Web 服务,它用 OpenAI API 做一个简单的“会议纪要生成”功能。我们将用 hindsight 对它进行“无痛增强”,整个过程不超过 5 分钟。

第一步:安装与初始化

pip install hindsight openai flask

创建一个hindsight_config.py文件,配置你的 API Key 和日志路径:

# hindsight_config.py import os from hindsight import HindsightLogger # 从环境变量读取 Key,确保安全 OPENAI_API_KEY = os.getenv("OPENAI_API_KEY") ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY") GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") # 初始化全局 logger HINDSIGHT_LOGGER = HindsightLogger( db_path="./hindsight.db", # SQLite 文件路径 providers={ # 声明你将使用的模型提供商 "openai": OPENAI_API_KEY, "anthropic": ANTHROPIC_API_KEY, "gemini": GEMINI_API_KEY, } )

第二步:改造你的业务逻辑

假设你原来的app.py是这样的:

# app.py (原始版本) from flask import Flask, request, jsonify import openai app = Flask(__name__) @app.route('/summarize', methods=['POST']) def summarize_meeting(): data = request.get_json() transcript = data['transcript'] response = openai.chat.completions.create( model="gpt-4-turbo", messages=[ {"role": "system", "content": "你是一个专业的会议纪要助手,请用中文生成一份简洁、重点突出的纪要。"}, {"role": "user", "content": f"会议录音文字稿:{transcript}"} ] ) summary = response.choices[0].message.content return jsonify({"summary": summary})

现在,用 hindsight 改造它。改动只有 5 行:

# app.py (hindsight 增强版) from flask import Flask, request, jsonify, g import openai from hindsight_config import HINDSIGHT_LOGGER # 导入 logger from hindsight import call_llm # 导入统一调用函数 app = Flask(__name__) @app.before_request def before_request(): """为每个请求生成唯一的 request_id,并存入 Flask g 对象""" import uuid g.request_id = str(uuid.uuid4()) @app.route('/summarize', methods=['POST']) def summarize_meeting(): data = request.get_json() transcript = data['transcript'] # 1. 构造 messages messages = [ {"role": "system", "content": "你是一个专业的会议纪要助手,请用中文生成一份简洁、重点突出的纪要。"}, {"role": "user", "content": f"会议录音文字稿:{transcript}"} ] # 2. 使用统一接口调用模型(不再是 openai.chat.completions.create) llm_response = call_llm( model="gpt-4-turbo", messages=messages, system_prompt="你是一个专业的会议纪要助手...", temperature=0.3, # 更低的 temperature,让纪要更确定 max_tokens=512 ) # 3. 检查是否有错误 if llm_response.error: return jsonify({"error": llm_response.error}), 500 # 4. 生成 summary summary = llm_response.text # 5. 记录 hindsight 日志!这是最关键的一步 HINDSIGHT_LOGGER.log_record( request_id=g.request_id, task_type="meeting_summary", business_id=data.get("meeting_id", "unknown"), # 关联业务 ID user_id=data.get("user_id", "anonymous"), model_provider="openai", model_name="gpt-4-turbo", messages=messages, system_prompt="你是一个专业的会议纪要助手...", response_text=summary, response_tokens=llm_response.tokens_used, response_time_ms=llm_response.latency_ms, # human_rating 和 is_corrected 可以留空,后续人工补充 ) return jsonify({"summary": summary})

就这么简单。你没有改变任何业务逻辑,只是把openai.chat.completions.create替换成了call_llm(),并在最后加了一行HINDSIGHT_LOGGER.log_record(...)。这行代码,就是你通往“可复盘 AI”的大门钥匙。

第三步:验证与查看日志

启动你的 Flask 服务,用 curl 发送一个测试请求:

curl -X POST http://localhost:5000/summarize \ -H "Content-Type: application/json" \ -d '{"transcript": "今天讨论了Q3的市场推广计划。张三建议加大抖音投放,李四认为应该聚焦微信公众号。王五提出可以做一个A/B测试...", "meeting_id": "MTG-2024-001", "user_id": "U12345"}'

几秒钟后,你会在控制台看到返回的 summary。同时,打开你的./hindsight.db文件(可以用 DB Browser for SQLite 工具),你会看到records表里多了一条新纪录。它的prompt_hash字段是e8a3b7c...,response_text字段里是生成的纪要,response_time_ms是1423.5。这一切,都是自动发生的。

实操心得:第一次集成时,最大的坑是忘记在app.before_request里设置g.request_id。HindsightLogger会尝试从flask.g里读取它,如果读不到,就会 fallback 到uuid.uuid4(),但这会导致同一个请求的多个日志(比如,一个请求里调用了两次 LLM)无法被关联。所以,before_request这个钩子,是集成的“必选项”,不是“可选项”。

4.2 自动生成复盘报告:用 Pandas 和 Matplotlib 画出你的 AI 健康度仪表盘

有了日志,下一步就是分析。hindsight 自带一个HindsightAnalyzer类,它把最常用的分析模式封装成了几个开箱即用的方法。我们以“周度复盘”为例,展示如何用不到 20 行代码,生成一份有洞见的 PDF 报告。

# weekly_report.py from hindsight import HindsightAnalyzer import matplotlib.pyplot as plt import pandas as pd from datetime import datetime, timedelta # 初始化 analyzer,指向你的日志数据库 analyzer = HindsightAnalyzer(db_path="./hindsight.db") # 获取过去 7 天的数据 end_date = datetime.now() start_date = end_date - timedelta(days=7) df = analyzer.get_records_by_time_range(start_date, end_date) # 1. 模型使用分布图 plt.figure(figsize=(12, 8)) plt.subplot(2, 2, 1) provider_counts = df['model_provider'].value_counts() plt.pie(provider_counts.values, labels=provider_counts.index, autopct='%1.1f%%') plt.title('Model Provider Distribution') # 2. 响应时间分布直方图 plt.subplot(2, 2, 2) plt.hist(df['response_time_ms'], bins=30, alpha=0.7, edgecolor='black') plt.xlabel('Response Time (ms)') plt.ylabel('Count') plt.title('Response Time Distribution') plt.axvline(df['response_time_ms'].mean(), color='r', linestyle='dashed', linewidth=1, label=f'Mean: {df["response_time_ms"].mean():.0f}ms') plt.legend() # 3. 任务类型成功率(成功定义为 non-empty response_text) plt.subplot(2, 2, 3) df['is_success'] = df['response_text'].str.len() > 0 success_rate = df.groupby('task_type')['is_success'].mean() success_rate.plot(kind='bar') plt.title('Success Rate by Task Type') plt.ylabel('Success Rate') # 4. 人工评分趋势(如果有填写) plt.subplot(2, 2, 4) if 'human_rating' in df.columns and not df['human_rating'].isna().all(): df['date'] = pd.to_datetime(df['timestamp']).dt.date daily_avg_rating = df.groupby('date')['human_rating'].mean() daily_avg_rating.plot(marker='o') plt.title('Average Human Rating Over Time') plt.ylabel('Rating (1-5)') plt.xticks(rotation=45) else: plt.text(0.5, 0.5, 'No human ratings available', ha='center', va='center', transform=plt.gca().transAxes) plt.title('Human Rating Trend') plt.tight_layout() plt.savefig('weekly_hindsight_report.png', dpi=300, bbox_inches='tight') print("Weekly report saved as 'weekly_hindsight_report.png'")

运行这个脚本,你会得到一张 2x2 的 PNG 图表,它包含了四个核心维度:谁在干活(Provider 分布)、干得快不快(响应时间)、干得对不对(成功率)、干得好不好(人工评分)。这张图,就是你向老板汇报 AI 项目进展的“一页纸精华”。

更进一步,你可以把这个脚本包装成一个 CLI 工具:

# 安装 click 库 pip install click # 创建 hindsight-report 命令 @click.command() @click.option('--days', default=7, help='Number of days for the report.') @click.option('--output', default='report.pdf', help='Output file path.') def generate_report(days, output): # ... 上面的分析逻辑 ... plt.savefig(output, dpi=300, bbox_inches='tight') print(f"Report generated: {output}") if __name__ == '__main__': generate_report()

然后,你就可以在终端里一键生成报告:

python weekly_report.py --days 30 --output monthly_report.pdf

这个自动化报告流程,彻底改变了我们团队的复盘文化。以前,复盘会是每周五下午两小时的“吐槽大会”;现在,它变成了每周一上午 10 点,大家围在白板前,看着这份 PDF,指着图表上的一个异常峰值,冷静地说:“看,上周三下午 3 点,Gemini 的成功率突然跌到 40%,我们去查一下那个时段的prompt_hash,看看是不是有人改了系统提示词。”

4.3 深度复盘实战:一次真实的“Gemini 登录失败”故障排查

网络热词里,“gemini登录”、“gemini出了点问题”、“your account is not eligible for gemini code assist” 频繁出现,这反映了 Gemini 的准入门槛和稳定性问题。下面,我用一个真实的案例,演示 hindsight 如何帮你把一个模糊的“报错”定位到具体的、可修复的代码行。

故障现象:上周五,我们的内部代码助手服务(基于 Gemini)突然大面积报错,错误信息是Your account is not eligible for gemini code assist for individuals at this time。前端用户看到的是一个友好的“服务暂时不可用”,但后台日志里,全是这个英文错误。

排查步骤:

  1. 第一步:在 hindsight 数据库里,筛选出所有model_provider='gemini'且error_message包含not eligible的记录。SQL 很简单:

    SELECT * FROM records WHERE model_provider = 'gemini' AND error_message LIKE '%not eligible%' AND timestamp > '2024-05-20 00:00:00' ORDER BY timestamp DESC LIMIT 10;

    结果显示,所有失败的请求,model_name都是gemini-pro,而成功的请求,model_name是gemini-1.5-flash。

  2. 第二步:对比成功与失败请求的messages字段。我们导出两条典型记录的messages,用diff工具对比。发现一个细微差别:失败的请求,messages数组里,user消息的content字段,开头多了一个不可见的 Unicode 字符U+FEFF(Byte Order Mark)。这个 BOM 字符,是某些 Windows 编辑器(如 Notepad

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询