feat: support choice+open dual question generation with judgment anchors
- Add judgment and followupHints fields to QuestionBankItem entity - Rewrite generateQuestions prompt for 3:7 choice:open ratio - Extract parseGeneratedQuestion function with type-aware parsing - Add 29 unit tests: 14 prompt content + 15 parse logic - Total: 97 tests passing (59 baseline + 38 new)
This commit is contained in:
@@ -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<string, QuestionDimension> = {
|
||||
'prompt': QuestionDimension.PROMPT,
|
||||
'llm': QuestionDimension.LLM,
|
||||
'ide': QuestionDimension.IDE,
|
||||
'devpattern': QuestionDimension.DEV_PATTERN,
|
||||
'workcapability': QuestionDimension.WORK_CAPABILITY,
|
||||
};
|
||||
|
||||
const DIFFICULTY_MAP: Record<string, QuestionDifficulty> = {
|
||||
'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<string, string> = {
|
||||
'prompt': 'PROMPT',
|
||||
'llm': 'LLM',
|
||||
'ide': 'IDE',
|
||||
'devPattern': 'DEV_PATTERN',
|
||||
'workCapability': 'WORK_CAPABILITY',
|
||||
};
|
||||
|
||||
const difficultyMap: Record<string, string> = {
|
||||
'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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user