| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366 |
- <?php
- namespace App\Services;
- use App\Models\PromptTemplate;
- use Illuminate\Support\Facades\Log;
- class PromptService
- {
- private const DEFAULT_QUESTION_PROMPT_NAME = 'question_generation_default';
- private const DEFAULT_QUESTION_PROMPT_TYPE = 'question_generation';
- private const DEFAULT_ENRICH_PROMPT_NAME = 'question_enrich_default';
- private const DEFAULT_ENRICH_PROMPT_TYPE = 'question_enrich';
- private const DEFAULT_SOLUTION_PROMPT_NAME = 'question_solution_regen_default';
- private const DEFAULT_SOLUTION_PROMPT_TYPE = 'question_solution_regen';
- /**
- * 获取提示词列表
- */
- public function listPrompts(?string $type = null, ?string $active = null): array
- {
- if ($type === null || $type === self::DEFAULT_QUESTION_PROMPT_TYPE) {
- $this->ensureDefaultQuestionPrompt();
- }
- if ($type === null || $type === self::DEFAULT_ENRICH_PROMPT_TYPE) {
- $this->ensureDefaultEnrichPrompt();
- }
- if ($type === null || $type === self::DEFAULT_SOLUTION_PROMPT_TYPE) {
- $this->ensureDefaultSolutionPrompt();
- }
- $query = PromptTemplate::query();
- if ($type) {
- $query->where('template_type', $type);
- }
- if ($active !== null) {
- $query->where('is_active', $active === 'yes' || $active === true);
- }
- return $query->orderByDesc('updated_at')
- ->get()
- ->map(fn (PromptTemplate $prompt) => $this->mapPrompt($prompt))
- ->values()
- ->all();
- }
- /**
- * 保存提示词
- */
- public function savePrompt(array $data): array
- {
- try {
- $templateName = (string) ($data['template_name'] ?? '');
- if ($templateName === '') {
- return ['success' => false, 'message' => '模板名称不能为空'];
- }
- $payload = [
- 'template_type' => $data['template_type'] ?? 'question_generation',
- 'template_content' => $data['template_content'] ?? '',
- 'variables' => $this->parseJsonField($data['variables'] ?? []),
- 'description' => $data['description'] ?? null,
- 'tags' => $this->parseJsonField($data['tags'] ?? []),
- 'is_active' => ($data['is_active'] ?? 'yes') === 'yes' || ($data['is_active'] === true),
- ];
- PromptTemplate::updateOrCreate(
- ['template_name' => $templateName],
- $payload
- );
- return ['success' => true, 'message' => '提示词已保存'];
- } catch (\Exception $e) {
- Log::error('保存提示词异常', [
- 'error' => $e->getMessage()
- ]);
- return ['success' => false, 'message' => '保存失败'];
- }
- }
- public function deletePrompt(string $templateName): bool
- {
- return PromptTemplate::where('template_name', $templateName)->delete() > 0;
- }
- public function importFromArray(array $prompts): array
- {
- $imported = 0;
- $updated = 0;
- $errors = [];
- foreach ($prompts as $prompt) {
- $templateName = (string) ($prompt['template_name'] ?? $prompt['name'] ?? '');
- if ($templateName === '') {
- continue;
- }
- $payload = [
- 'template_name' => $templateName,
- 'template_type' => $prompt['template_type'] ?? $prompt['type'] ?? 'question_generation',
- 'template_content' => $prompt['template_content'] ?? '',
- 'variables' => $this->parseJsonField($prompt['variables'] ?? []),
- 'description' => $prompt['description'] ?? null,
- 'tags' => $this->parseJsonField($prompt['tags'] ?? []),
- 'is_active' => ($prompt['is_active'] ?? 'yes') === 'yes' || ($prompt['is_active'] === true),
- ];
- try {
- $existing = PromptTemplate::query()->where('template_name', $templateName)->first();
- PromptTemplate::updateOrCreate(['template_name' => $templateName], $payload);
- $existing ? $updated++ : $imported++;
- } catch (\Throwable $e) {
- $errors[] = $templateName . ': ' . $e->getMessage();
- }
- }
- return [
- 'success' => true,
- 'imported' => $imported,
- 'updated' => $updated,
- 'errors' => $errors,
- ];
- }
- public function getPrompt(string $templateName): ?array
- {
- $prompt = PromptTemplate::where('template_name', $templateName)->first();
- return $prompt ? $this->mapPrompt($prompt) : null;
- }
- public function getPromptContent(string $templateName): ?string
- {
- $prompt = PromptTemplate::where('template_name', $templateName)->first();
- return $prompt?->template_content;
- }
- /**
- * 获取默认提示词模板
- */
- public function getDefaultPromptTemplate(): string
- {
- return '你是资深的中学数学命题专家,请为{knowledge_point}知识点生成高质量题目。
- 【核心要求】
- 1. 题目必须符合{grade_level}年级水平
- 2. 难度分布:基础({basic_ratio}%) + 中等({intermediate_ratio}%) + 拔高({advanced_ratio}%)
- 3. 题型分配:选择题({choice}道) + 填空题({fill}道) + 解答题({solution}道)
- 【难度提示(来源于卷子位置)】
- - 若提供题目编号/位置:{question_index} / {question_position_hint}
- - 可据此对难度系数做初步判断:卷首偏基础,卷中偏中等,卷末偏拔高
- 【技能覆盖】
- {skill_coverage}
- 【图示处理】
- - 如果题目涉及图形/示意图/坐标系/几何草图,必须在题干内内嵌一段完整的 <svg> 标签来还原图形;不要使用外链图片、base64 或占位符。
- - SVG 要包含明确的宽高(建议 260~360 像素),只使用基础图元(line、rect、circle、polygon、path、text),并给出必要的坐标、角点和标注文本。
- - 确保题干文本描述与 SVG 一致,例如“如图所示”后紧跟 SVG,且 SVG 放在题干末尾即可被前端直接渲染。
- 【质量标准】
- - 准确性:100%正确
- - 多样性:避免重复
- - 梯度性:难度递进合理
- - 实用性:贴近实际应用
- 【输出格式】
- {
- "total": {count},
- "questions": [
- {
- "id": "唯一标识",
- "stem": "题干",
- "answer": "标准答案",
- "solution": ["详细解答1", "详细解答2", "详细解答3"],
- "difficulty": 0.6,
- "question_type": "choice/fill/answer",
- "skills": ["技能点1", "技能点2"],
- "knowledge_points": ["{knowledge_point}"]
- }
- ]
- }';
- }
- public function getDefaultEnrichPromptTemplate(): string
- {
- return '你是一名“数学题目完善助手”。给定原题干与可选图片外链,请补全题目关键信息并做轻量修订。
- 要求:
- - 只输出 JSON
- - 必须包含字段:stem, options, answer, solution, question_type, difficulty, knowledge_points, solution_steps
- - 可选字段:abilities(能力/技能点数组)
- - stem 只做轻微修订(排版/符号/错别字),不得改题意
- - 若提供图片外链(image_urls),可结合图片理解题意,但不要生成 SVG
- - question_type 仅允许:choice / fill / answer
- - answer 类题目必须提供 solution_steps(分步评分),每步包含 score 与 kp_codes
- - knowledge_points 为题目级知识点列表
- 材料内容(含题干与图片):
- {content}
- 输出 JSON 示例:
- {
- "stem": "...",
- "options": {"A": "...", "B": "..."},
- "answer": "...",
- "solution": "...",
- "question_type": "answer",
- "difficulty": 0.6,
- "knowledge_points": ["A01"],
- "solution_steps": [
- {"step_index": 1, "title": "...", "content": "...", "score": 4, "kp_codes": ["A01"]}
- ],
- "abilities": ["计算能力", "分析能力"]
- }';
- }
- public function getDefaultSolutionPromptTemplate(): string
- {
- return '你是一名“中学数学解题专家”。给定题干、正确答案与可选图片外链,请生成与答案一致的详细解题过程。
- 要求:
- - 只输出 JSON
- - 必须包含字段:solution, steps
- - solution 为简洁的整体思路(不写空话)
- - steps 为数组,每步包含:step_index, title, content, score, kp_codes
- - steps 必须给出可操作的计算/推理过程,避免“思想/方法类空话”
- - 若题型为选择/填空,也要给出关键推理步骤,但 steps 不能省略核心计算
- - 严格保证最终结论与 provided_answer 一致
- - 若提供图片外链(image_urls),可结合图片理解题意,但不要生成 SVG
- 题干:
- {stem}
- 正确答案:
- {provided_answer}
- 图片外链(如有):
- {image_urls}
- 输出 JSON 示例:
- {
- "solution": "整体解题思路概述",
- "steps": [
- {"step_index": 1, "title": "列式", "content": "...", "score": 4, "kp_codes": ["A01"]},
- {"step_index": 2, "title": "求解", "content": "...", "score": 6, "kp_codes": ["A01"]}
- ]
- }';
- }
- /**
- * 检查服务健康状态
- */
- public function checkHealth(): bool
- {
- return true;
- }
- private function parseJsonField($value): array
- {
- if (is_array($value)) {
- return $value;
- }
- if (is_string($value) && $value !== '') {
- try {
- $decoded = json_decode($value, true, 512, JSON_THROW_ON_ERROR);
- return is_array($decoded) ? $decoded : [];
- } catch (\Throwable) {
- return [];
- }
- }
- return [];
- }
- private function ensureDefaultQuestionPrompt(): void
- {
- $exists = PromptTemplate::query()
- ->where('template_name', self::DEFAULT_QUESTION_PROMPT_NAME)
- ->exists();
- if ($exists) {
- return;
- }
- $this->savePrompt([
- 'template_name' => self::DEFAULT_QUESTION_PROMPT_NAME,
- 'template_type' => self::DEFAULT_QUESTION_PROMPT_TYPE,
- 'template_content' => $this->getDefaultPromptTemplate(),
- 'variables' => json_encode([
- 'knowledge_point',
- 'grade_level',
- 'basic_ratio',
- 'intermediate_ratio',
- 'advanced_ratio',
- 'choice',
- 'fill',
- 'solution',
- 'skill_coverage',
- 'question_index',
- 'question_position_hint',
- 'count',
- ], JSON_UNESCAPED_UNICODE),
- 'description' => '默认知识点题目生成模板(题型+难度分布)',
- 'tags' => json_encode(['default', 'knowledge_point'], JSON_UNESCAPED_UNICODE),
- 'is_active' => 'yes',
- ]);
- }
- private function ensureDefaultEnrichPrompt(): void
- {
- $exists = PromptTemplate::query()
- ->where('template_name', self::DEFAULT_ENRICH_PROMPT_NAME)
- ->exists();
- if ($exists) {
- return;
- }
- $this->savePrompt([
- 'template_name' => self::DEFAULT_ENRICH_PROMPT_NAME,
- 'template_type' => self::DEFAULT_ENRICH_PROMPT_TYPE,
- 'template_content' => $this->getDefaultEnrichPromptTemplate(),
- 'variables' => json_encode(['content'], JSON_UNESCAPED_UNICODE),
- 'description' => '基于题干+图片的题目信息补全模板',
- 'tags' => json_encode(['default', 'enrich'], JSON_UNESCAPED_UNICODE),
- 'is_active' => 'yes',
- ]);
- }
- private function ensureDefaultSolutionPrompt(): void
- {
- $exists = PromptTemplate::query()
- ->where('template_name', self::DEFAULT_SOLUTION_PROMPT_NAME)
- ->exists();
- if ($exists) {
- return;
- }
- $this->savePrompt([
- 'template_name' => self::DEFAULT_SOLUTION_PROMPT_NAME,
- 'template_type' => self::DEFAULT_SOLUTION_PROMPT_TYPE,
- 'template_content' => $this->getDefaultSolutionPromptTemplate(),
- 'variables' => json_encode(['stem', 'provided_answer', 'image_urls'], JSON_UNESCAPED_UNICODE),
- 'description' => '基于答案重写解题思路的系统模板',
- 'tags' => json_encode(['default', 'solution'], JSON_UNESCAPED_UNICODE),
- 'is_active' => 'yes',
- ]);
- }
- private function mapPrompt(PromptTemplate $prompt): array
- {
- $variables = $prompt->variables ?? [];
- $tags = $prompt->tags ?? [];
- return [
- 'template_name' => $prompt->template_name,
- 'template_type' => $prompt->template_type,
- 'template_content' => $prompt->template_content,
- 'variables' => json_encode($variables, JSON_UNESCAPED_UNICODE),
- 'description' => $prompt->description,
- 'tags' => json_encode($tags, JSON_UNESCAPED_UNICODE),
- 'is_active' => $prompt->is_active ? 'yes' : 'no',
- 'updated_at' => $prompt->updated_at,
- ];
- }
- }
|