:搭建用戶口碑監(jiān)控與情緒分析管道)
你是不是也有過這樣的時刻昨天還覺得自己的編碼助手很聰明今天喂同一個需求它卻開始一本正經(jīng)地編造接口名。剛想吐槽“模型是不是被降智了”又看到群里有人附和再往下翻還會看到有人在 Hacker News 上貼出一個項目名字直接就叫“Show HN: Is AI Dumber Today? An index of AI model experience from users opinion”。這個問題問得直白也能刺痛很多正在把 AI 集成到生產(chǎn)流程里的工程師。因為“變笨”的體感確實存在但我們很難用“跑分下降了”來解釋它。模型版本沒變、Prompt 沒變、任務復雜度沒變可結(jié)果就是不如昨天穩(wěn)定。真正的問題或許不在于模型參數(shù)被偷偷改小而在于我們?nèi)鄙僖粭l觀測“實時體驗”的工程化管道。用戶在各種社區(qū)、評論區(qū)、工單里生產(chǎn)了大量零散觀點如果有一種辦法把這些觀點收攏、清洗、量化并形成類似指數(shù)的指標至少我們能知道這種“變笨”到底是主觀錯覺還是可觀測的系統(tǒng)性波動。這篇文章會拆解這類項目的兩個層面。第一是判斷層用戶口碑型模型體驗指數(shù)到底衡量什么它和傳統(tǒng) benchmark 評測差別在哪里。第二是工程層我會給出一個可復現(xiàn)的最小實現(xiàn)骨架包括 JSONL 數(shù)據(jù)格式、情緒分類、SQLite 存儲、滑動窗口指數(shù)計算和查詢 API。你不需要一開始就做大平臺先跑通一條評論到一張趨勢表的閉環(huán)比什么都重要。1. 為什么用戶總是覺得“AI 變笨了”“AI 變笨了”不是一個嚴謹?shù)募夹g(shù)命題但它是一個真實的用戶信號。如果你去翻看各種開發(fā)者反饋會發(fā)現(xiàn)絕大部分抱怨都指向幾個非常具體的現(xiàn)象對話剛開始不久模型就報“context length 超限”看起來像智能不足實際是長會話管理問題。API 返回 selected model is at capacity 或 model is unavailable模型本身沒問題但容量調(diào)度導致體驗斷裂??蛻舳伺c模型網(wǎng)關(guān)版本不一致出現(xiàn)“該模型不存在”或“模型名不識別”的報錯。開啟思維鏈推理的模型多輪對話沒有把 reasoning_content 回傳給 API導致后續(xù)一輪直接失敗。服務端臨時降級到小模型用戶在無感知的情況下被切換到更低能力版本。上面這些現(xiàn)象搜索材料里都能看到大量真實報錯原文。它們有一個共同點用戶往往不會去區(qū)分“模型能力下降”和“模型服務狀態(tài)異常”只會得到一個結(jié)論——今天這個 AI 好笨。用一張表格可以看得很清楚用戶看到的提示工程層面常見原因用戶實際感知selected model is at capacity服務端容量調(diào)度或流量過高半天不響應誤以為模型傻了model is unavailableProvider 網(wǎng)關(guān)異?;蚵酚墒」δ懿豢捎皿w驗中斷400 context length 超限上下文窗口被打滿模型“忘記”了之前的內(nèi)容reasoning_content 未回傳思維鏈模型多輪協(xié)議處理不當推理能力突然失效model not supported客戶端版本與模型名不匹配認為模型被悄悄下架這類體驗問題恰恰是 benchmark 很難覆蓋的。因為評測環(huán)境是干凈的固定測試集、固定超參數(shù)、固定上下文服務端也處于穩(wěn)定狀態(tài)。而真實用戶面對的是復雜網(wǎng)絡、長上下文、并發(fā)調(diào)度、模型版本灰度切換甚至還有客戶端配置差異。所以我的判斷是用戶覺得“AI 變笨”本質(zhì)上是用戶在觀察一個動態(tài)系統(tǒng)。模型能力是其中一個變量但不是全部變量。如果想要回答“AI 是不是變笨了”就非常需要有另一個數(shù)據(jù)入口——直接從用戶意見出發(fā)去度量一段時期內(nèi)真實交互中的體驗變化。這就是“模型體驗指數(shù)”類項目存在的價值。2. 從意見文本到“模型體驗指數(shù)”要解決什么問題先把這個概念拆開。“指數(shù)”在金融領(lǐng)域是用來反映一組標的綜合變化的統(tǒng)計量比如股票指數(shù)。放到大模型場景里我們要建模的對象不是股價而是某個模型在一段時間內(nèi)收到的用戶意見變化。一條意見可以來自一條帖子、一條評論、一個工單、一條反饋消息。把大量意見按照模型、平臺、時間、情緒傾向聚合起來就形成了“模型體驗指數(shù)”。這個指數(shù)想回答的問題非常聚焦“在這段時間里用過這個模型的用戶整體感受是變好了還是變壞了”要達成這個目標需要先處理三個現(xiàn)實挑戰(zhàn)。第一個挑戰(zhàn)是模型名歸一化。同一個模型在不同語境下叫法完全不同。有人寫 Claude Sonnet有人寫 claude-sonnet有人直接寫“克勞德”有人寫 GPT-4o有人寫 gpt4o。如果不做歸一化統(tǒng)計口徑會立刻失真。第二個挑戰(zhàn)是情緒判斷。一條評論說“This model is not buggy”規(guī)則系統(tǒng)如果只看 buggy 關(guān)鍵詞很可能把正面評價誤判成負面。這說明簡單的關(guān)鍵詞匹配一定有誤差需要在工程鏈路里設(shè)計兜底機制。第三個挑戰(zhàn)是時間粒度。模型體驗不是一成不變的服務端可能在一周內(nèi)完成數(shù)次灰度更新。如果按月度聚合很多短期波動會被平均掉。合理的做法是先把意見按天沉淀再通過滑動窗口計算連續(xù)指數(shù)讓短期異常能及時暴露出來。為了讓你快速建立感知這里給出一個最小意見條目應該包含的字段字段含義示例model_name原始模型名gpt-4oplatform來源平臺hacker_news / reddit / feedbackcontent評論或反饋正文今天好慢一直報錯sentiment情緒傾向1 正面 / 0 中性 / -1 負面category問題類型slow / buggy / refusesource_url來源鏈接可選created_at發(fā)生時間ISO8601 時間有了這個結(jié)構(gòu)采集、清洗、入庫、聚合就都能標準化了。后續(xù)章節(jié)我會沿著這條鏈路逐步實現(xiàn)一個最小可用的模型。3. 口碑型指數(shù)與 Benchmark 評測的本質(zhì)區(qū)別很多讀者會問市面上已經(jīng)有很多大模型評測基準像 MMLU、HumanEval、MT-Bench、LMArena還需要再做一套用戶口碑指數(shù)嗎我的觀點是兩者觀測的完全不是同一個東西。Benchmark 適合回答“模型在預設(shè)任務上達到什么水平”用戶口碑指數(shù)適合回答“模型在實際使用中體驗如何波動”。前者是能力快照后者是體驗溫度計。兩者的差異可以從多個維度看對比維度Benchmark 評測用戶口碑體驗指數(shù)數(shù)據(jù)來源固定測試集真實用戶文本意見時效性按版本或發(fā)布周期更新可按天或周滾動更新主觀偏差通過評測協(xié)議盡量控制天然存在需要歸一化處理能力維度準確率、推理、編碼等可用性、速度、拒絕率、幻覺等受服務影響較小很大容量與路由異常會直接體現(xiàn)典型用途模型選型、論文對比線上質(zhì)量監(jiān)測、客服體驗看板Benchmark 的“失效”并不是說它沒價值而是它覆蓋不到生產(chǎn)環(huán)境中的高頻擾動。比如某個模型在 HumanEval 上得分很高但它在高并發(fā)時頻繁觸發(fā)容量拒絕某個模型推理能力很強但它會在多輪對話中因為上下文管理問題頻繁斷開。這些體驗問題用戶不會寫成“今天 Negative Log Likelihood 上升了”只會說“這模型真笨”。指數(shù)型項目本質(zhì)上是在給 benchmark 補一個“用戶在真實環(huán)境里實際感受到什么”的觀測維度。它不做能力絕對值的排名而是做體驗相對變化的追蹤。理解了這一點你就不會問“這個指數(shù)能不能替代 Chatbot Arena”這類問題了它倆本就不該是替代關(guān)系。4. 數(shù)據(jù)源與合規(guī)邊界不能見到什么就爬什么做用戶口碑指數(shù)最容易被忽略但又最重要的問題是數(shù)據(jù)從哪里來。常見來源大致有這么幾類公開社區(qū)Hacker News、Reddit、相關(guān)開發(fā)者論壇適合觀察大眾輿論。開發(fā)者平臺GitHub Issues、產(chǎn)品 Feedback 區(qū)適合追蹤特定編程工具的問題。自有產(chǎn)品反饋你所在團隊收到的用戶反饋、工單、問卷這是最可信、權(quán)益最清晰的數(shù)據(jù)。社交平臺內(nèi)容密度高但 API 限制嚴格數(shù)據(jù)獲取和合規(guī)成本都很高。如果你只是個人追蹤幾個主流模型的表現(xiàn)我更推薦從 Hacker News、Reddit 這類有公開接口或歷史數(shù)據(jù)集的平臺開始。最小閉環(huán)不需要實時流可以先用導出的歷史評論跑通流程再逐步增加定時采集。這里必須強調(diào)幾條合規(guī)底線第一采集行為要遵守平臺服務條款和 robots 協(xié)議。不要寫一個無限并發(fā)的爬蟲去抓取全站數(shù)據(jù)這對源站是負擔也容易給自己帶來法律風險。第二涉及個人信息的評論要脫敏。展示聚合結(jié)果時不要展示能夠直接定位到具體用戶的 ID、郵箱、聯(lián)系方式。第三用戶原文可以用于內(nèi)部統(tǒng)計但如果要公開轉(zhuǎn)發(fā)或引用需要謹慎評估版權(quán)和平臺規(guī)則。最小原型階段建議只做聚合展示不做原文墻。第四生產(chǎn)級系統(tǒng)要設(shè)置合理的采集頻率和重試退避。對公開 API 要控制 QPS盡量使用官方提供的分頁參數(shù)不做暴力翻頁。一句話總結(jié)做體驗指數(shù)的第一步不是寫爬蟲而是先想清楚數(shù)據(jù)的權(quán)利邊界。數(shù)據(jù)來源不穩(wěn)定后面所有工程都是白做。5. 系統(tǒng)整體設(shè)計與 SQLite 數(shù)據(jù)結(jié)構(gòu)在動手寫代碼之前先把系統(tǒng)分清楚層級。一個最小可用的模型體驗指數(shù)系統(tǒng)通常包含五層接入層接收 JSONL、CSV或API 返回的半結(jié)構(gòu)化評論。清洗層做模型名歸一化、正文去重、無效評論過濾。分類層根據(jù)關(guān)鍵詞或調(diào)用模型判斷情緒和問題類別。存儲層把標準化的意見條目寫入數(shù)據(jù)庫。服務層提供指數(shù)查詢 API 和可視化數(shù)據(jù)。原型階段我建議直接用 SQLite 而不是 MySQL 或 PostgreSQL。原因很簡單單文件、零部署、事務支持夠用適合先把閉環(huán)跑通。等數(shù)據(jù)量到百萬級、需要多人并發(fā)寫入時再遷移到 PostgreSQL 也不遲。建表語句如下-- 文件路徑schema.sql CREATE TABLE IF NOT EXISTS model_opinions ( id INTEGER PRIMARY KEY AUTOINCREMENT, model_name TEXT NOT NULL, platform TEXT NOT NULL DEFAULT , content TEXT NOT NULL, category TEXT NOT NULL DEFAULT , sentiment INTEGER NOT NULL DEFAULT 0, source_url TEXT NOT NULL DEFAULT , created_at TEXT NOT NULL ); CREATE INDEX IF NOT EXISTS idx_model_time ON model_opinions (model_name, created_at); CREATE INDEX IF NOT EXISTS idx_platform ON model_opinions (platform);這里有三個設(shè)計細節(jié)值得說明。第一created_at 直接使用 TEXT 類型保存 ISO8601 時間。SQLite 的 date 函數(shù)可以直接處理這種格式減少轉(zhuǎn)換成本。但要注意統(tǒng)一使用 UTC 時間避免不同地區(qū)服務器寫入的時間口徑不一致。第二模型名雖然要保留原始名稱用于追溯但統(tǒng)計時應該使用歸一化后的標準名。為了簡單我直接把歸一化結(jié)果寫回 model_name 字段原始名稱可以在采集層單獨保存原型階段可以不建額外表。第三創(chuàng)建復合索引 idx_model_time是為了支撐最常見查詢根據(jù)模型名查詢一段時間內(nèi)的意見分布。如果后續(xù)要按平臺過濾再繼續(xù)加索引。6. 環(huán)境準備與項目目錄本文的代碼需要 Python 3.8 以上環(huán)境推薦使用 3.10 或更高版本。SQLite 本身是 Python 標準庫內(nèi)置模塊不需要單獨安裝。如果希望把指數(shù)通過 HTTP 接口暴露出來還需要安裝 Flask。建議先創(chuàng)建項目目錄mkdir model-experience-index cd model-experience-index python3 -m venv venv source venv/bin/activateWindows 環(huán)境下激活虛擬環(huán)境的命令為venv\Scripts\activate然后安裝依賴pip install flask requestsrequirements.txt 可以這樣寫requests2.31.0 flask3.0.0版本號可以根據(jù)你本地的實際環(huán)境微調(diào)不需要嚴格鎖定。這里的 requests 主要用于從公開接口拉取數(shù)據(jù)如果你已經(jīng)有現(xiàn)成的 JSONL 文件甚至可以只保留 Flask。最終的目錄結(jié)構(gòu)如下model-experience-index/ ├── schema.sql ├── model_index_pipeline.py ├── index_query.py ├── app.py ├── data/ │ ├── raw.jsonl │ └── opinions.db └── requirements.txtdata 目錄需要手動創(chuàng)建mkdir data7. 清洗與分類把一段評論變成帶標簽的數(shù)據(jù)這一階段的輸入是 data/raw.jsonl一行一條 JSON格式如下{model_name: gpt-4o, platform: dev_blog, content: The model refused to answer a simple coding question today, very disappointed., created_at: 2025-06-01T10:00:00Z, source_url: https://example.com/post/1} {model_name: Claude Sonnet, platform: hacker_news, content: Claude is so helpful. It solved my refactoring task accurately., created_at: 2025-06-01T11:00:00Z, source_url: https://example.com/post/2} {model_name: gpt-4o, platform: dev_blog, content: timeout again, the model is at capacity, I cannot continue my work., created_at: 2025-06-02T09:00:00Z, source_url: https://example.com/post/3}實際采集時created_at 請使用真實的采集時間或評論發(fā)布時間。下面這段代碼會完成三個任務讀取 JSONL、歸一化模型名、基于關(guān)鍵詞規(guī)則完成情緒分類。# 文件路徑model_index_pipeline.py # -*- coding: utf-8 -*- import argparse import json import re import sqlite3 from datetime import datetime, timezone from pathlib import Path # 模型名歸一化映射表 MODEL_ALIASES { gpt-4o: gpt-4o, gpt4o: gpt-4o, gpt-4o-mini: gpt-4o-mini, gpt4o-mini: gpt-4o-mini, claude: claude, claude-sonnet: claude-sonnet, claude sonnet: claude-sonnet, deepseek-r1: deepseek-r1, deepseek r1: deepseek-r1, } # 問題類別與關(guān)鍵詞注意這里不做復雜 NLP POSITIVE_WORDS { helpful: [helpful, impressive, amazing, accurate, great answer, works perfectly, 好用, 準確, 驚艷], fast: [fast, snappy, instant, no latency, 非常快, 流暢], } NEGATIVE_WORDS { refuse: [refuse, cannot answer, cant answer, not allowed, 拒絕回答, 無法回答, sorry], buggy: [bug, wrong, incorrect, false, hallucination, 編造, 胡說, 錯誤, 幻覺, 不對, 有問題], slow: [slow, timeout, lag, at capacity, unavailable, 卡頓, 超時, 無法連接, 斷連], } def normalize_model(raw_name: str) - str: 把不同寫法歸一化成標準模型名。 if not raw_name: return unknown key raw_name.strip().lower() return MODEL_ALIASES.get(key, key) def classify_and_score(text: str): 基于關(guān)鍵詞做粗分類返回 (category, sentiment)。 sentiment: 1 表示正面-1 表示負面0 表示中性或混合。 category: 多個類別使用逗號拼接。 lowered (text or ).lower() pos_hits, neg_hits [], [] for cat, words in NEGATIVE_WORDS.items(): for word in words: if word in lowered: neg_hits.append(cat) break for cat, words in POSITIVE_WORDS.items(): for word in words: if word in lowered: pos_hits.append(cat) break if pos_hits and neg_hits: return mixed, 0 if neg_hits: return ,.join(neg_hits), -1 if pos_hits: return ,.join(pos_hits), 1 return uncategorized, 0 def process_raw_file(raw_file: str, db_file: str) - int: 讀取 JSONL 文件并寫入 SQLite。 db sqlite3.connect(db_file) db.execute( CREATE TABLE IF NOT EXISTS model_opinions ( id INTEGER PRIMARY KEY AUTOINCREMENT, model_name TEXT NOT NULL, platform TEXT NOT NULL DEFAULT , content TEXT NOT NULL, category TEXT NOT NULL DEFAULT , sentiment INTEGER NOT NULL DEFAULT 0, source_url TEXT NOT NULL DEFAULT , created_at TEXT NOT NULL ) ) inserted 0 with open(raw_file, r, encodingutf-8) as fh: for line in fh: line line.strip() if not line: continue item json.loads(line) content (item.get(content) or ).strip() if len(content) 3: continue raw_model item.get(model_name, ) model_name normalize_model(raw_model) platform (item.get(platform) or unknown).strip() source_url (item.get(source_url) or ).strip() created_at item.get(created_at) if not created_at: created_at datetime.now(timezone.utc).isoformat(timespecseconds) category, sentiment classify_and_score(content) db.execute( INSERT INTO model_opinions (model_name, platform, content, category, sentiment, source_url, created_at) VALUES (?, ?, ?, ?, ?, ?, ?) , (model_name, platform, content, category, sentiment, source_url, created_at), ) inserted 1 db.commit() db.close() return inserted if __name__ __main__: parser argparse.ArgumentParser(description模型體驗指數(shù) - 數(shù)據(jù)清洗入庫) parser.add_argument(--input, defaultdata/raw.jsonl, help輸入 JSONL 文件) parser.add_argument(--db, defaultdata/opinions.db, helpSQLite 數(shù)據(jù)庫文件) args parser.parse_args() total process_raw_file(args.input, args.db) print(f清洗入庫完成共插入 {total} 條意見)代碼里需要解釋幾個關(guān)鍵點。MODEL_ALIASES 映射表用于統(tǒng)一大小寫和常見縮寫實際生產(chǎn)環(huán)境中這份映射表應當做成獨立配置文件方便在不改代碼的前提下擴充。classify_and_score 函數(shù)基于關(guān)鍵詞做粗分類它只能作為基線不能期待它達到 GPT-4 級別的語義理解。但它的好處是確定性強、可解釋、無額外成本原型階段完全夠用。情緒判定時的正負同時命中會被歸為 mixed這比簡單把正負相加更穩(wěn)妥。試想一條評論寫“It is fast but often wrong”它既包含“fast”又包含“wrong”如果只統(tǒng)計關(guān)鍵詞數(shù)量可能被算成正樣本這是失真。歸為混合中性至少不會污染情緒匯總。代碼里的一處易錯點是在寫 SQL 時不能使用普通字符串格式化拼接評論內(nèi)容而是必須使用問號占位符。因為真實評論里經(jīng)常出現(xiàn)百分號、單引號等特殊字符如果用 % 格式化去拼 SQL就會出現(xiàn)類似 unsupported format character 的異常。參數(shù)化查詢可以徹底規(guī)避這個問題。執(zhí)行命令如下python model_index_pipeline.py --input data/raw.jsonl --db data/opinions.db如果一切正常會看到輸出清洗入庫完成共插入 3 條意見此時可以用 SQLite 命令驗證sqlite3 data/opinions.db SELECT model_name, category, sentiment, created_at FROM model_opinions;你會看到類似于下面的結(jié)果gpt-4o|refuse|-1|2025-06-01T10:00:00Z claude-sonnet|helpful|1|2025-06-01T11:00:00Z gpt-4o|slow|-1|2025-06-02T09:00:00Z8. 指數(shù)計算用滑動窗口觀察趨勢清洗入庫只是第一步。真正有價值的是把離散的意見聚合成可觀察的曲線。這里定義一種簡單的指數(shù)在某個時間窗口內(nèi)負面意見數(shù)量占該窗口總意見數(shù)的比例乘以 100。數(shù)值越高代表用戶負面體驗占比越高。為了避免單日樣本過少帶來的劇烈抖動代碼里加入了滑動窗口和最小樣本量門檻。# 文件路徑index_query.py # -*- coding: utf-8 -*- import argparse import sqlite3 from collections import defaultdict from datetime import date, datetime, timedelta def compute_index(db_file: str, model_name: str, window: int 14, min_samples: int 3): 計算模型在滑動窗口內(nèi)的負面體驗指數(shù)。 index 越高表示該時間段內(nèi)用戶負面評價占比越高。 conn sqlite3.connect(db_file) conn.row_factory sqlite3.Row rows conn.execute( SELECT date(created_at) AS day, sentiment, COUNT(*) AS cnt FROM model_opinions WHERE model_name ? GROUP BY date(created_at), sentiment ORDER BY day , (model_name,), ).fetchall() conn.close() daily_agg defaultdict(lambda: {pos: 0, neg: 0, neu: 0}) for row in rows: day row[day] if row[sentiment] 1: daily_agg[day][pos] row[cnt] elif row[sentiment] -1: daily_agg[day][neg] row[cnt] else: daily_agg[day][neu] row[cnt] if not daily_agg: return [] start_day datetime.strptime(min(daily_agg.keys()), %Y-%m-%d).date() end_day datetime.strptime(max(daily_agg.keys()), %Y-%m-%d).date() series [] # buffer 中只保留當前滑動窗口內(nèi)的日聚合數(shù)據(jù) buffer defaultdict(lambda: {pos: 0, neg: 0, neu: 0}) def clean_buffer(current_day: date): expired [d for d in buffer if (current_day - d).days window] for d in expired: del buffer[d] current start_day while current end_day: key current.isoformat() clean_buffer(current) if key in daily_agg: buffer[current] daily_agg[key] else: buffer[current] {pos: 0, neg: 0, neu: 0} total sum(buffer[d][pos] buffer[d][neg] buffer[d][neu] for d in buffer) neg sum(buffer[d][neg] for d in buffer) if total min_samples: index_value round(neg * 100.0 / total, 2) series.append({ date: key, index: index_value, samples: total, }) current timedelta(days1) return series if __name__ __main__: parser argparse.ArgumentParser(description模型體驗指數(shù) - 查詢與計算) parser.add_argument(--db, defaultdata/opinions.db) parser.add_argument(--model, defaultgpt-4o) parser.add_argument(--window, typeint, default14) parser.add_argument(--min-samples, typeint, default3) args parser.parse_args() result compute_index(args.db, args.model, args.window, args.min_samples) if not result: print(沒有找到該模型的數(shù)據(jù)請檢查模型名是否拼寫正確) raise SystemExit(1) print(f{date:12}{index:10}{samples}) for item in result: print(f{item[date]:12}{item[index]:10}{item[samples]})這段代碼在理解上有三個重點。第一SQL 使用 date(created_at) 把 ISO8601 時間字符串截斷到天方便按天聚合。GROUP BY 中的 sentiment 能統(tǒng)計每日正負中三條數(shù)量這樣可以減少 Python 層的計算量。第二buffer 保存的是滑動窗口內(nèi)的數(shù)據(jù)。每進入一個新日期先淘汰超過窗口天數(shù)的舊數(shù)據(jù)再加入當天數(shù)據(jù)再計算當前窗口的負面比例。這和移動平均的思想是一致的能有效平滑單日波動。第三min_samples 參數(shù)的用意是防止樣本太少時指數(shù)失真。如果某一天只有一條負面評論負面率就是 100%這顯然不能說明模型突然變差了。只有當窗口內(nèi)總樣本數(shù)達到門檻時才輸出指數(shù)。執(zhí)行查詢命令python index_query.py --db data/opinions.db --model gpt-4o --window 14示意輸出如下date index samples 2025-06-01 50.0 2 2025-06-02 66.67 3這里 2025-06-01 有兩條例樣本一條負面一條正面所以指數(shù)是 502025-06-02 有三條例樣本其中兩條負面所以指數(shù)約 66.67。如果你的數(shù)據(jù)量更大曲線會平滑很多。9. 提供一個輕量的 HTTP 查詢接口聚合計算完成后下一步自然是通過接口暴露給前端或監(jiān)控系統(tǒng)。這里用 Flask 寫一個最小 API。# 文件路徑app.py # -*- coding: utf-8 -*- from flask import Flask, jsonify, request from index_query import compute_index app Flask(__name__) app.route(/api/index, methods[GET]) def api_index(): model_name request.args.get(model, gpt-4o) window request.args.get(window, 14, typeint) min_samples request.args.get(min_samples, 3, typeint) series compute_index( data/opinions.db, model_namemodel_name, windowwindow, min_samplesmin_samples, ) if not series: return jsonify({model: model_name, error: no enough data or unknown model}), 404 return jsonify({model: model_name, series: series}) if __name__ __main__: app.run(host0.0.0.0, port8000, debugFalse)啟動服務python app.py然后在另一個終端執(zhí)行curl http://127.0.0.1:8000/api/index?modelgpt-4owindow14預期返回一段 JSON核心結(jié)構(gòu)如下{ model: gpt-4o, series: [ {date: 2025-06-01, index: 50.0, samples: 2}, {date: 2025-06-02, index: 66.67,