[阶段4.4] AI 建议结果缓存 - 页面不再阻塞等待 160 秒
网关首选的推理模型一次生成约 160 秒,每次打开建议页都重跑不可用。 结果落库缓存,页面读缓存,用户想要新的再手动触发。 db.py: - 新增 ai_recommendations 表,每用户一行(重新生成是替换不是累积) - fingerprint 列记录这条建议是基于哪份数据算出来的 services/analysis.py: - _fingerprint() 对全部每日指标 + 运动条数取 sha256,任何一次同步 新增或修正了数值都会让摘要变化,从而使缓存失效 - TTL 默认 24 小时(AI_CACHE_TTL_HOURS 可调) - 指定 model 参数时绕过缓存:点名某个模型意味着想要那个模型的答案 - 降级到规则引擎的结果不写缓存,避免把兜底答案当成 AI 结果存下来 - 缓存写入失败只打日志,不影响本次请求返回 routes: ?refresh=1 强制重新生成 前端: - "重新生成" 按钮走 refresh,并提示需要 1-3 分钟、可以离开本页 - meta 栏显示是否为缓存结果及生成时间,以及网关的上游厂商 - axios 该请求超时放宽到 240s(冷生成远超默认超时) tests/test_ai_cache.py (20 通过): - 第二次调用不再打模型 - 新增一天数据 / 修正某天数值 / 新增一条运动记录,三种情况都失效 - TTL 边界两侧各一条(刚过期重算、未过期沿用) - 缓存按用户隔离,A 的结果不会答给 B - payload 损坏时重新生成而不是抛异常 - 规则兜底结果和无数据用户都不落缓存 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -17,15 +17,19 @@ function Recommendations() {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const load = useCallback(async (model?: string) => {
|
||||
const [regenerating, setRegenerating] = useState(false);
|
||||
|
||||
const load = useCallback(async (model?: string, refresh?: boolean) => {
|
||||
setLoading(true);
|
||||
setRegenerating(Boolean(refresh || model));
|
||||
setError('');
|
||||
try {
|
||||
setResult(await apiClient.getAiRecommendations(model || undefined));
|
||||
setResult(await apiClient.getAiRecommendations(model || undefined, refresh));
|
||||
} catch (err: any) {
|
||||
setError(errorMessage(err, '获取建议失败'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
setRegenerating(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
@@ -73,13 +77,20 @@ function Recommendations() {
|
||||
|
||||
<button
|
||||
className="btn btn-primary"
|
||||
onClick={() => load(selected)}
|
||||
onClick={() => load(selected, true)}
|
||||
disabled={loading}
|
||||
>
|
||||
{loading ? '生成中…' : '重新生成'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{regenerating && (
|
||||
<div className="notice">
|
||||
正在请求大模型重新分析,通常需要 1-3 分钟(推理模型会先推演再作答)。
|
||||
期间可以离开本页,结果会被缓存下来。
|
||||
</div>
|
||||
)}
|
||||
|
||||
{configured.length === 0 && models.length > 0 && (
|
||||
<div className="notice">
|
||||
尚未配置任何模型密钥。在 <code>backend/.env</code> 中填入
|
||||
@@ -94,7 +105,16 @@ function Recommendations() {
|
||||
<div className={`meta-bar ${meta.source}`}>
|
||||
{meta.source === 'ai' ? (
|
||||
<>
|
||||
<strong>AI 生成</strong> · 模型 {meta.model} · 分析了 {meta.days} 天数据
|
||||
<strong>AI 生成</strong> · 模型 {meta.model}
|
||||
{meta.upstream && `(上游 ${meta.upstream})`}
|
||||
{' · '}分析了 {meta.days} 天数据
|
||||
{meta.cached && (
|
||||
<span className="fallback">
|
||||
· 缓存结果
|
||||
{meta.generatedAt &&
|
||||
`,生成于 ${new Date(meta.generatedAt + 'Z').toLocaleString('zh-CN')}`}
|
||||
</span>
|
||||
)}
|
||||
{meta.fallbackFrom && meta.fallbackFrom.length > 0 && (
|
||||
<span className="fallback">
|
||||
({meta.fallbackFrom.join('、')} 失败后自动切换)
|
||||
|
||||
@@ -51,9 +51,13 @@ export interface AiRecommendations {
|
||||
source: 'ai' | 'rules';
|
||||
model: string | null;
|
||||
provider?: string;
|
||||
upstream?: string | null;
|
||||
days?: number;
|
||||
fallbackFrom?: string[];
|
||||
reason?: string;
|
||||
/** True when served from the stored answer rather than freshly generated. */
|
||||
cached?: boolean;
|
||||
generatedAt?: string;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -207,10 +211,19 @@ class ApiClient {
|
||||
return data;
|
||||
}
|
||||
|
||||
async getAiRecommendations(model?: string, days?: number) {
|
||||
/**
|
||||
* Served from the stored answer unless `refresh` is set or a `model` is
|
||||
* named. A fresh generation can take minutes, so the caller should show a
|
||||
* long-running state for those two cases.
|
||||
*/
|
||||
async getAiRecommendations(model?: string, refresh?: boolean, days?: number) {
|
||||
const { data } = await this.client.get<AiRecommendations>(
|
||||
'/analysis/ai-recommendations',
|
||||
{ params: { model, days } }
|
||||
{
|
||||
params: { model, days, ...(refresh ? { refresh: 1 } : {}) },
|
||||
// A cold generation runs well past axios's default timeout.
|
||||
timeout: 240_000,
|
||||
}
|
||||
);
|
||||
return data;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user