Fix training plan generation flow
这个提交包含在:
@@ -8,6 +8,50 @@ import { invokeLLM } from "./_core/llm";
|
||||
import { storagePut } from "./storage";
|
||||
import * as db from "./db";
|
||||
import { nanoid } from "nanoid";
|
||||
import {
|
||||
normalizeAdjustedPlanResponse,
|
||||
normalizeTrainingPlanResponse,
|
||||
} from "./trainingPlan";
|
||||
|
||||
async function invokeStructuredPlan<T>(params: {
|
||||
baseMessages: Array<{ role: "system" | "user"; content: string }>;
|
||||
responseFormat: {
|
||||
type: "json_schema";
|
||||
json_schema: {
|
||||
name: string;
|
||||
strict: true;
|
||||
schema: Record<string, unknown>;
|
||||
};
|
||||
};
|
||||
parse: (content: unknown) => T;
|
||||
}) {
|
||||
let lastError: unknown;
|
||||
|
||||
for (let attempt = 0; attempt < 3; attempt++) {
|
||||
const retryHint =
|
||||
attempt === 0 || !(lastError instanceof Error)
|
||||
? []
|
||||
: [{
|
||||
role: "user" as const,
|
||||
content:
|
||||
`上一次输出无法被系统解析,错误是:${lastError.message}。` +
|
||||
"请只返回一个合法、完整、可解析的 JSON 对象,不要包含额外说明、注释或 Markdown 代码块。",
|
||||
}];
|
||||
|
||||
const response = await invokeLLM({
|
||||
messages: [...params.baseMessages, ...retryHint],
|
||||
response_format: params.responseFormat,
|
||||
});
|
||||
|
||||
try {
|
||||
return params.parse(response.choices[0]?.message?.content);
|
||||
} catch (error) {
|
||||
lastError = error;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError instanceof Error ? lastError : new Error("Failed to parse structured LLM response");
|
||||
}
|
||||
|
||||
export const appRouter = router({
|
||||
system: systemRouter,
|
||||
@@ -85,12 +129,12 @@ ${recentScores.length > 0 ? `- 用户最近的分析数据: ${JSON.stringify(rec
|
||||
|
||||
请返回JSON格式,包含每天的训练内容。`;
|
||||
|
||||
const response = await invokeLLM({
|
||||
messages: [
|
||||
const parsed = await invokeStructuredPlan({
|
||||
baseMessages: [
|
||||
{ role: "system", content: "你是网球训练计划生成器。返回严格的JSON格式。" },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
response_format: {
|
||||
responseFormat: {
|
||||
type: "json_schema",
|
||||
json_schema: {
|
||||
name: "training_plan",
|
||||
@@ -123,12 +167,12 @@ ${recentScores.length > 0 ? `- 用户最近的分析数据: ${JSON.stringify(rec
|
||||
},
|
||||
},
|
||||
},
|
||||
parse: (content) => normalizeTrainingPlanResponse({
|
||||
content,
|
||||
fallbackTitle: `${input.durationDays}天训练计划`,
|
||||
}),
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content;
|
||||
const parsed = typeof content === "string" ? JSON.parse(content) : null;
|
||||
if (!parsed) throw new Error("Failed to generate training plan");
|
||||
|
||||
const planId = await db.createTrainingPlan({
|
||||
userId: user.id,
|
||||
title: parsed.title,
|
||||
@@ -173,12 +217,12 @@ ${recentScores.length > 0 ? `- 用户最近的分析数据: ${JSON.stringify(rec
|
||||
|
||||
请根据分析结果调整训练计划,增加针对薄弱环节的训练,返回与原计划相同格式的JSON。`;
|
||||
|
||||
const response = await invokeLLM({
|
||||
messages: [
|
||||
{ role: "system", content: "你是网球评分生成器。返回严格的JSON格式。" },
|
||||
const parsed = await invokeStructuredPlan({
|
||||
baseMessages: [
|
||||
{ role: "system", content: "你是网球训练计划调整器。返回严格的JSON格式。" },
|
||||
{ role: "user", content: prompt },
|
||||
],
|
||||
response_format: {
|
||||
responseFormat: {
|
||||
type: "json_schema",
|
||||
json_schema: {
|
||||
name: "adjusted_plan",
|
||||
@@ -212,12 +256,12 @@ ${recentScores.length > 0 ? `- 用户最近的分析数据: ${JSON.stringify(rec
|
||||
},
|
||||
},
|
||||
},
|
||||
parse: (content) => normalizeAdjustedPlanResponse({
|
||||
content,
|
||||
fallbackTitle: currentPlan.title,
|
||||
}),
|
||||
});
|
||||
|
||||
const content = response.choices[0]?.message?.content;
|
||||
const parsed = typeof content === "string" ? JSON.parse(content) : null;
|
||||
if (!parsed) throw new Error("Failed to adjust plan");
|
||||
|
||||
await db.updateTrainingPlan(input.planId, {
|
||||
exercises: parsed.exercises,
|
||||
adjustmentNotes: parsed.adjustmentNotes,
|
||||
|
||||
在新工单中引用
屏蔽一个用户