diff --git a/server/src/assessment/entities/question-bank-item.entity.spec.ts b/server/src/assessment/entities/question-bank-item.entity.spec.ts new file mode 100644 index 0000000..16ca2ad --- /dev/null +++ b/server/src/assessment/entities/question-bank-item.entity.spec.ts @@ -0,0 +1,85 @@ +import { QuestionBankItem, QuestionType, QuestionDifficulty, QuestionDimension, QuestionBankItemStatus } from './question-bank-item.entity'; + +describe('QuestionBankItem entity', () => { + describe('existing fields', () => { + it('should create an instance with default questionType', () => { + const item = new QuestionBankItem(); + expect(item.questionType).toBeUndefined(); + }); + + it('should set and get basic fields', () => { + const item = new QuestionBankItem(); + item.questionText = '【场景】你在编写代码... 【问题】请描述你会如何处理'; + item.questionType = QuestionType.SHORT_ANSWER; + item.options = null; + item.correctAnswer = null; + item.keyPoints = ['规范文档化', '源头统一']; + item.difficulty = QuestionDifficulty.STANDARD; + item.dimension = QuestionDimension.PROMPT; + item.basis = '知识库原文依据'; + item.status = QuestionBankItemStatus.PENDING_REVIEW; + + expect(item.questionText).toBe('【场景】你在编写代码... 【问题】请描述你会如何处理'); + expect(item.questionType).toBe(QuestionType.SHORT_ANSWER); + expect(item.options).toBeNull(); + expect(item.correctAnswer).toBeNull(); + expect(item.keyPoints).toEqual(['规范文档化', '源头统一']); + expect(item.difficulty).toBe(QuestionDifficulty.STANDARD); + expect(item.dimension).toBe(QuestionDimension.PROMPT); + expect(item.basis).toBe('知识库原文依据'); + expect(item.status).toBe(QuestionBankItemStatus.PENDING_REVIEW); + }); + }); + + describe('judgment field', () => { + it('should accept judgment text for choice question', () => { + const item = new QuestionBankItem(); + item.judgment = 'B正确,因为提供了具体约束和角色设定。A错误在于过于笼统。C错误在于过度细节但缺乏核心约束。D错误在于错误建议。'; + expect(item.judgment).toBe('B正确,因为提供了具体约束和角色设定。A错误在于过于笼统。C错误在于过度细节但缺乏核心约束。D错误在于错误建议。'); + }); + + it('should accept judgment text for open question', () => { + const item = new QuestionBankItem(); + item.judgment = '关键考点:会话管理——长对话导致上下文窗口膨胀 通过标准:说出"让AI总结之前内容+开新窗口"即通过'; + expect(item.judgment).toContain('通过标准'); + expect(item.judgment).toContain('会话管理'); + }); + + it('should allow null judgment', () => { + const item = new QuestionBankItem(); + item.judgment = null; + expect(item.judgment).toBeNull(); + }); + }); + + describe('followupHints field', () => { + it('should accept array of followup hints', () => { + const item = new QuestionBankItem(); + item.followupHints = [ + '如果只回答"开新窗口"没说怎么带上前情:追问"开新窗口后之前讨论的结论不就丢了吗?怎么把有用信息带过去?"', + '如果内容不完整:追问"还有没有更好的办法?"', + ]; + expect(item.followupHints).toHaveLength(2); + expect(item.followupHints[0]).toContain('开新窗口'); + expect(item.followupHints[1]).toContain('更好的办法'); + }); + + it('should accept single followup hint', () => { + const item = new QuestionBankItem(); + item.followupHints = ['追问如何保留之前结论']; + expect(item.followupHints).toHaveLength(1); + }); + + it('should accept empty array', () => { + const item = new QuestionBankItem(); + item.followupHints = []; + expect(item.followupHints).toHaveLength(0); + }); + + it('should allow null followupHints', () => { + const item = new QuestionBankItem(); + item.followupHints = null; + expect(item.followupHints).toBeNull(); + }); + }); +}); diff --git a/server/src/assessment/entities/question-bank-item.entity.ts b/server/src/assessment/entities/question-bank-item.entity.ts index f3e13ab..fe52df9 100644 --- a/server/src/assessment/entities/question-bank-item.entity.ts +++ b/server/src/assessment/entities/question-bank-item.entity.ts @@ -86,6 +86,12 @@ export class QuestionBankItem { @Column({ type: 'text', nullable: true }) basis: string | null; + @Column({ type: 'text', nullable: true }) + judgment: string | null; + + @Column({ type: 'simple-json', nullable: true, name: 'followup_hints' }) + followupHints: string[] | null; + @Column({ name: 'created_by', nullable: true, type: 'text' }) createdBy: string | null; diff --git a/server/src/assessment/services/question-bank.service.spec.ts b/server/src/assessment/services/question-bank.service.spec.ts new file mode 100644 index 0000000..5cab47a --- /dev/null +++ b/server/src/assessment/services/question-bank.service.spec.ts @@ -0,0 +1,261 @@ +import { + GENERATE_QUESTIONS_SYSTEM_PROMPT, + parseGeneratedQuestion, +} from './question-bank.service'; +import { + QuestionBankItem, + QuestionType, + QuestionDifficulty, + QuestionDimension, + QuestionBankItemStatus, +} from '../entities/question-bank-item.entity'; + +const BANK_ID = 'test-bank-id'; + +describe('GENERATE_QUESTIONS_SYSTEM_PROMPT', () => { + it('should require both choice and open question types', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toContain('choice'); + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toContain('open'); + }); + + it('should specify choice:open ratio', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toMatch(/3.*7|choice.*open|选择题.*简答题/); + }); + + it('should require judgment field for every question', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toContain('judgment'); + }); + + it('should require followupHints for open questions', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toContain('followupHints'); + }); + + it('should include a few-shot example for choice questions', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toMatch(/代码规范|AGENTS\.md|Prettier/); + }); + + it('should include a few-shot example for open questions', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toMatch(/会话管理|上下文窗口|开新窗口/); + }); + + it('should prohibit concept-definition questions', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toMatch(/禁止.*概念|不要.*定义|不能.*什么是/); + }); + + it('should require similar option lengths', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toMatch(/字符差|选项.*长度|长度.*相近/); + }); + + it('should prohibit "以上都对" and "以上都不对"', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toMatch(/禁止.*以上都对|以上都对.*禁止|禁止.*以上都不对/); + }); + + it('should require keyPoints from knowledge base', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toMatch(/key_points.*知识库|知识库.*key_points|知识库.*原文/); + }); + + it('should prohibit markdown wrapping in JSON output', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toMatch(/不要.*[Mm]arkdown|禁止.*[Mm]arkdown|不允许.*[Mm]arkdown|只输出.*JSON|纯JSON/); + }); + + it('should allow difficulty STANDARD, ADVANCED, SPECIALIST', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toContain('STANDARD'); + }); + + it('should allow five dimensions: prompt, llm, ide, devPattern, workCapability', () => { + expect(GENERATE_QUESTIONS_SYSTEM_PROMPT).toMatch(/prompt|llm|ide|devPattern|workCapability/); + }); + + it('should have reasonable prompt length', () => { + const len = GENERATE_QUESTIONS_SYSTEM_PROMPT.length; + expect(len).toBeGreaterThan(1500); + expect(len).toBeLessThan(8000); + }); +}); + +const mockChoiceQuestion = { + type: 'choice', + scenario: '你在编写代码,AI生成的代码风格不一致', + questionText: '【场景】你在编写一段复杂代码... 【问题】以下哪种做法最有效?', + options: ['A. 每次手动调整', 'B. 写入AGENTS.md', 'C. 用通用指令', 'D. Prettier格式化'], + correctAnswer: 'B', + judgment: 'B正确,因为规范文档化能从源头统一。A效率低。C模糊。D只解决表面问题。', + keyPoints: ['规范文档化', '源头统一'], + difficulty: 'STANDARD', + dimension: 'prompt', + basis: '知识库原文', +}; + +const mockOpenQuestion = { + type: 'open', + scenario: '你与AI反复修改文档30轮后AI开始遗忘关键约束', + questionText: '【场景】你与AI反复修改... 【问题】这种情况怎么造成的?应该怎么做?', + judgment: '关键考点:会话管理 通过标准:说出让AI总结+开新窗口即通过', + followupHints: ['追问如何保留之前结论'], + keyPoints: ['上下文窗口膨胀', '信息蒸馏'], + difficulty: 'STANDARD', + dimension: 'prompt', + basis: '知识库原文', +}; + +describe('parseGeneratedQuestion', () => { + describe('choice type', () => { + it('should parse choice question with MULTIPLE_CHOICE type', () => { + const item = parseGeneratedQuestion(mockChoiceQuestion, BANK_ID); + + expect(item.questionType).toBe(QuestionType.MULTIPLE_CHOICE); + expect(item.options).toEqual([ + 'A. 每次手动调整', + 'B. 写入AGENTS.md', + 'C. 用通用指令', + 'D. Prettier格式化', + ]); + expect(item.options).toHaveLength(4); + expect(item.correctAnswer).toBe('B'); + expect(item.judgment).toContain('B正确'); + expect(item.followupHints).toBeNull(); + }); + + it('should store judgment for choice question', () => { + const item = parseGeneratedQuestion(mockChoiceQuestion, BANK_ID); + + expect(item.judgment).toBe( + 'B正确,因为规范文档化能从源头统一。A效率低。C模糊。D只解决表面问题。', + ); + }); + + it('should store keyPoints with technique tag', () => { + const q = { + ...mockChoiceQuestion, + technique: '代码风格注入', + }; + const item = parseGeneratedQuestion(q, BANK_ID); + + expect(item.keyPoints[0]).toBe('【考查技巧】代码风格注入'); + expect(item.keyPoints).toContain('规范文档化'); + expect(item.keyPoints).toContain('源头统一'); + }); + }); + + describe('open type', () => { + it('should parse open question with SHORT_ANSWER type', () => { + const item = parseGeneratedQuestion(mockOpenQuestion, BANK_ID); + + expect(item.questionType).toBe(QuestionType.SHORT_ANSWER); + expect(item.options).toBeNull(); + expect(item.correctAnswer).toBeNull(); + expect(item.judgment).toContain('通过标准'); + expect(item.judgment).toContain('会话管理'); + }); + + it('should store followupHints array', () => { + const item = parseGeneratedQuestion(mockOpenQuestion, BANK_ID); + + expect(item.followupHints).toEqual(['追问如何保留之前结论']); + expect(item.followupHints).toHaveLength(1); + }); + + it('should handle open question with no followupHints', () => { + const q = { ...mockOpenQuestion, followupHints: [] }; + const item = parseGeneratedQuestion(q, BANK_ID); + + expect(item.followupHints).toEqual([]); + }); + + it('should handle open question with 2 followupHints', () => { + const q = { + ...mockOpenQuestion, + followupHints: ['追问1', '追问2'], + }; + const item = parseGeneratedQuestion(q, BANK_ID); + + expect(item.followupHints).toHaveLength(2); + }); + }); + + describe('common fields', () => { + it('should store keyPoints on both types', () => { + const choice = parseGeneratedQuestion(mockChoiceQuestion, BANK_ID); + const open = parseGeneratedQuestion(mockOpenQuestion, BANK_ID); + + expect(choice.keyPoints.length).toBeGreaterThan(0); + expect(open.keyPoints.length).toBeGreaterThan(0); + }); + + it('should handle missing keyPoints gracefully', () => { + const q = { ...mockOpenQuestion, keyPoints: undefined }; + const item = parseGeneratedQuestion(q, BANK_ID); + + expect(item.keyPoints).toEqual([]); + }); + + it('should normalize dimension case-insensitively', () => { + const q1 = parseGeneratedQuestion( + { ...mockOpenQuestion, dimension: 'LLM' }, + BANK_ID, + ); + const q2 = parseGeneratedQuestion( + { ...mockOpenQuestion, dimension: 'llm' }, + BANK_ID, + ); + const q3 = parseGeneratedQuestion( + { ...mockOpenQuestion, dimension: 'Llm' }, + BANK_ID, + ); + + expect(q1.dimension).toBe(QuestionDimension.LLM); + expect(q2.dimension).toBe(QuestionDimension.LLM); + expect(q3.dimension).toBe(QuestionDimension.LLM); + }); + + it('should default dimension to WORK_CAPABILITY for unknown values', () => { + const q = parseGeneratedQuestion( + { ...mockOpenQuestion, dimension: 'unknown' }, + BANK_ID, + ); + + expect(q.dimension).toBe(QuestionDimension.WORK_CAPABILITY); + }); + + it('should map all five dimensions correctly', () => { + const dims = ['prompt', 'llm', 'ide', 'devPattern', 'workCapability']; + const expected = [ + QuestionDimension.PROMPT, + QuestionDimension.LLM, + QuestionDimension.IDE, + QuestionDimension.DEV_PATTERN, + QuestionDimension.WORK_CAPABILITY, + ]; + + dims.forEach((dim, i) => { + const q = parseGeneratedQuestion( + { ...mockOpenQuestion, dimension: dim }, + BANK_ID, + ); + expect(q.dimension).toBe(expected[i]); + }); + }); + + it('should store difficulty correctly', () => { + const q = parseGeneratedQuestion( + { ...mockOpenQuestion, difficulty: 'ADVANCED' }, + BANK_ID, + ); + + expect(q.difficulty).toBe(QuestionDifficulty.ADVANCED); + }); + + it('should set bankId and status on all items', () => { + const item = parseGeneratedQuestion(mockOpenQuestion, BANK_ID); + + expect(item.bankId).toBe(BANK_ID); + expect(item.status).toBe(QuestionBankItemStatus.PENDING_REVIEW); + }); + + it('should store basis text', () => { + const item = parseGeneratedQuestion(mockChoiceQuestion, BANK_ID); + + expect(item.basis).toBe('知识库原文'); + }); + }); +}); diff --git a/server/src/assessment/services/question-bank.service.ts b/server/src/assessment/services/question-bank.service.ts index 700208f..42de9f1 100644 --- a/server/src/assessment/services/question-bank.service.ts +++ b/server/src/assessment/services/question-bank.service.ts @@ -69,6 +69,180 @@ const DIMENSIONS = [ QuestionDimension.WORK_CAPABILITY, ]; +export const GENERATE_QUESTIONS_SYSTEM_PROMPT = `你是 AI 人才考核的出题专家。你需要从知识库内容中生成考核题目。 + +## 一、内部步骤(在脑中完成,不要输出) +1. 从知识库提取可考核的实战知识点 +2. 确定该知识点对应的具体技巧或方法 +3. 围绕该技巧设计一个真实工作场景 + +## 二、题型比例 +本题库同时生成两种题型,按 **choice:open = 3:7** 分配。 +- choice = 选择题(4选1) +- open = 简答题(开放式 + 追问) + +## 三、选择题规则(choice 型) +### 3.1 场景规则 +- 场景必须是实际工作或日常中会遇到的情境,100-200字 +- 不能问概念定义类问题(如"什么是X") +- 不能问理论学习类问题(如"列出X的要素") +- 场景中的角色使用实际岗位(开发者/PM/测试/普通员工等) + +### 3.2 决策点规则 +- 每道题必须有明确的决策点——学习者要做选择或决定怎么做 +- 不能只是"请解释" + +### 3.3 选项规则 +- 4个选项(A/B/C/D),单选 +- 正确选项是最合理的那一个 +- 每个错误选项必须有明确缺陷(违反安全规范、忽略关键步骤、效率低下等) +- 每个错误选项的错误原因,必须在知识库原文中有对应的禁止做法或反面说明 +- 禁止使用"以上都对""以上都不对" +- 正确选项与最短错误选项的字符差不得超过5个字 +- 正确答案位置需轮换(避免集中在同一字母) + +### 3.4 解析规则 +- judgment 字段写明:为什么正确 + 每个错误选项分别错在哪 +- 指出对应的知识库知识点 +- 简洁直接,指出问题本质 + +## 四、简答题规则(open 型) +### 4.1 场景规则 +- 同选择题 3.1 +- 场景中暗示需要什么能力,但不要说破 + +### 4.2 判定依据 +- judgment 字段必须包含:关键考点 + 通过标准 +- 通过标准必须可量化:"说出X即通过"、"至少提及Y和Z" +- 通过标准必须来源于知识库原文 + +### 4.3 追问方向 +- followupHints 数组:0-2条追问方向 +- 追问用于引导学习者补充遗漏的关键点 +- 追问应具体、可回答 +- 示例:"如果只回答开新窗口没说怎么带上前情:追问怎么把有用信息带过去?" + +## 五、禁止项(适用于所有题型) +- 禁止问概念定义(如"什么是提示词工程") +- 禁止问理论列举(如"六要素有哪些") +- 禁止选择题出现"以上都对""以上都不对" +- 禁止正确选项明显比其他选项长或短 +- 禁止场景脱离实际(如"如果你是CEO"不适合L1) +- 禁止虚构知识库中不存在的方法、工具、术语 +- key_points 必须从知识库原文中提取,不得自行编造 +- 相邻题目的场景背景不得重复或相似 + +## 六、出题维度(自动判断) +根据题目内容,从以下五个维度中选择最匹配的一个: +- prompt(提示词工程) +- llm(LLM理解) +- ide(IDE协作开发) +- devPattern(开发范式) +- workCapability(工作能力) + +## 七、难度说明 +默认 STANDARD。如果场景特别复杂或涉及多步推理,可标记 ADVANCED 或 SPECIALIST。 + +## 八、参考示例 + +### 选择题示例 +【场景】你在编写一段复杂的业务逻辑代码,让 AI 帮忙生成。AI 第一次生成的代码功能没问题,但代码风格和你项目现有的不太一样(缩进方式、命名规范不同)。为了提高后续生成的代码一致性,以下哪种做法最有效? + +A. 每次生成后手动调整格式,下次再让 AI 生成时重新说明一遍风格要求。 +B. 将项目的代码规范写入 AGENTS.md 或项目配置文件中,让 AI 在生成时自动参考。 +C. 给 AI 发送一条"请遵循团队规范"的通用指令,下一条代码就会自动匹配风格。 +D. 等全部代码生成完后,统一用 Prettier 或 ESLint 格式化工具修正所有风格问题。 + +**正确答案:B** + +**解析:** B正确,将规范文档化并注入上下文,能从源头统一AI的输出风格。A效率低且容易遗漏。C"团队规范"是模糊描述,AI无法知道具体指什么。D格式化工具只能解决缩进等表面问题,无法修复命名规范等逻辑性规范。 + +### 简答题示例 +【场景】你正在同一个 AI 对话窗口里和 AI 反复修改一份技术方案文档。改了大概30轮之后,你发现 AI 开始"忘记"一开始定下的某些关键约束条件。比如你最早说过"目标读者是业务部门,不要写太多技术细节",但 AI 新生成的内容又开始出现大量技术术语。 + +【问题】这种情况是怎么造成的?你应该怎么做才能让 AI 重新聚焦? + +**判定依据:** +- 关键考点:会话管理——长对话导致上下文窗口膨胀,AI注意力分散 +- 通过标准:说出"让AI总结之前内容+开新窗口"即通过 + +**追问方向:** +- 如果只回答"开新窗口"没说怎么带上前情:追问"开新窗口后之前讨论的结论不就丢了吗?怎么把有用信息带过去?" +- 如果内容不完整:追问"还有没有更好的办法?" + +## 九、输出格式(仅输出纯JSON,不要带Markdown标记) + +选择题输出: +{ + "type": "choice", + "scenario": "场景描述(100-200字实际工作场景)", + "questionText": "【场景】... 【问题】以下哪种做法最有效?", + "options": ["A. 选项A描述", "B. 选项B描述", "C. 选项C描述", "D. 选项D描述"], + "correctAnswer": "B", + "judgment": "B正确,因为... A错误在于... C错误在于... D错误在于...", + "keyPoints": ["知识库中的评分要素1", "知识库中的评分要素2"], + "difficulty": "STANDARD", + "dimension": "prompt", + "basis": "知识库原文依据" +} + +简答题输出: +{ + "type": "open", + "scenario": "场景描述(100-200字实际工作场景)", + "questionText": "【场景】... 【问题】请描述你会如何处理", + "judgment": "关键考点:XXX 通过标准:说出XXX即通过", + "followupHints": ["追问方向1", "追问方向2"], + "keyPoints": ["知识库中的评分要素1"], + "difficulty": "STANDARD", + "dimension": "prompt", + "basis": "知识库原文依据" +} + +输出为JSON数组:`; + +const DIMENSION_MAP: Record = { + 'prompt': QuestionDimension.PROMPT, + 'llm': QuestionDimension.LLM, + 'ide': QuestionDimension.IDE, + 'devpattern': QuestionDimension.DEV_PATTERN, + 'workcapability': QuestionDimension.WORK_CAPABILITY, +}; + +const DIFFICULTY_MAP: Record = { + 'STANDARD': QuestionDifficulty.STANDARD, + 'ADVANCED': QuestionDifficulty.ADVANCED, + 'SPECIALIST': QuestionDifficulty.SPECIALIST, +}; + +export function parseGeneratedQuestion( + q: any, + bankId: string, +): QuestionBankItem { + const isChoice = q.type === 'choice'; + const dimension = DIMENSION_MAP[q.dimension?.toLowerCase()] ?? QuestionDimension.WORK_CAPABILITY; + const difficulty = DIFFICULTY_MAP[q.difficulty?.toUpperCase()] ?? QuestionDifficulty.STANDARD; + const techniqueTag = q.technique ? `【考查技巧】${q.technique}` : null; + const keyPoints = techniqueTag + ? [techniqueTag, ...(q.keyPoints ?? q.key_points ?? [])] + : (q.keyPoints ?? q.key_points ?? []); + + return { + bankId, + questionText: q.questionText ?? q.question_text ?? '', + questionType: isChoice ? QuestionType.MULTIPLE_CHOICE : QuestionType.SHORT_ANSWER, + options: isChoice ? (q.options ?? null) : null, + correctAnswer: isChoice ? (q.correctAnswer ?? null) : null, + judgment: q.judgment ?? null, + followupHints: isChoice ? null : (q.followupHints ?? null), + keyPoints, + difficulty, + dimension, + basis: q.basis ?? null, + status: QuestionBankItemStatus.PENDING_REVIEW, + } as QuestionBankItem; +} + @Injectable() export class QuestionBankService { private readonly logger = new Logger(QuestionBankService.name); @@ -301,37 +475,8 @@ export class QuestionBankService { }, }); - const systemPrompt = `你是一个实战考核设计专家。你要做三件事(在脑中完成,不要输出中间过程)。 - -### 内部步骤(不要输出): -1. 从知识库提取可考核的实战知识点 -2. 确定该知识点对应的**具体技巧或方法**,这将成为考核目标 -3. 围绕该技巧设计一个真实工作场景 - -### 出题规则: -- 题目格式:"【场景】具体场景描述 【问题】请描述你会如何处理" -- 场景中暗示需要什么能力,但不要说破 -- **绝对禁止**概念题、选择题 - -### 评分标准来源(严格遵守): -- key_points 必须从知识库原文中提取,不得自行编造评分标准 -- 每个 key_point 必须是知识库中明确提及的要素 -- **禁止**添加知识库中没有的方法、工具、格式(如 Markdown) - -### 只输出 JSON: -[ - { - "knowledge_points": ["知识点原文"], - "technique": "此题考查的具体技巧名称", - "scenario": "实战场景", - "question_text": "【场景】... 【问题】请描述你会如何...", - "key_points": ["知识库中的评分要素", "知识库中的评分要素"], - "difficulty": "STANDARD", - "dimension": "prompt|llm|ide|devPattern|workCapability", - "basis": "知识库原文依据" - } -]`; - const humanMsg = `【知识库内容 - 唯一来源】\n\n--- 开始 ---\n${knowledgeBaseContent}\n--- 结束 ---\n\n请按三步流程生成 ${count} 道简答题。难度以 STANDARD 为主。`; + const systemPrompt = GENERATE_QUESTIONS_SYSTEM_PROMPT; + const humanMsg = `【知识库内容 - 唯一来源】\n\n--- 开始 ---\n${knowledgeBaseContent}\n--- 结束 ---\n\n请按上述规则生成 ${count} 道题,choice:open 比例约 3:7。难度以 STANDARD 为主。`; try { const response = await model.invoke([ @@ -349,39 +494,11 @@ export class QuestionBankService { parsedQuestions = [parsedQuestions]; } - const dimensionMap: Record = { - 'prompt': 'PROMPT', - 'llm': 'LLM', - 'ide': 'IDE', - 'devPattern': 'DEV_PATTERN', - 'workCapability': 'WORK_CAPABILITY', - }; - - const difficultyMap: Record = { - 'STANDARD': 'STANDARD', - 'ADVANCED': 'ADVANCED', - 'SPECIALIST': 'SPECIALIST', - }; - const items: QuestionBankItem[] = []; for (const q of parsedQuestions) { - const dimension = dimensionMap[q.dimension?.toLowerCase()] || 'WORK_CAPABILITY'; - const difficulty = difficultyMap[q.difficulty?.toUpperCase()] || 'STANDARD'; - const techniqueTag = q.technique ? `【考查技巧】${q.technique}` : null; - const keyPoints = techniqueTag - ? [techniqueTag, ...(q.key_points || [])] - : (q.key_points || []); - - const item = this.itemRepository.create({ - bankId, - questionText: q.question_text, - questionType: QuestionType.SHORT_ANSWER, - keyPoints, - difficulty: difficulty as QuestionDifficulty, - dimension: dimension as QuestionDimension, - basis: q.basis, - status: QuestionBankItemStatus.PENDING_REVIEW, - }); + const item = this.itemRepository.create( + parseGeneratedQuestion(q, bankId), + ); items.push(item); }