18 KiB
智能填空題系統開發計劃
基於
智能填空題系統設計規格.md制定的詳細實施計劃
📋 開發階段總覽
Phase 1: 資料庫結構調整 (預計 0.5 天)
Phase 2: 後端服務開發 (預計 2 天)
Phase 3: 前端組件優化 (預計 1 天)
Phase 4: 測試與優化 (預計 1 天)
Phase 1: 資料庫結構調整
🎯 目標
為 Flashcard 實體添加 FilledQuestionText 欄位,支援儲存挖空後的題目
📝 具體任務
1.1 更新實體模型
檔案: backend/DramaLing.Api/Models/Entities/Flashcard.cs
[MaxLength(1000)]
public string? FilledQuestionText { get; set; } // 挖空後的題目文字
1.2 資料庫 Migration
命令: dotnet ef migrations add AddFilledQuestionText
檔案: backend/DramaLing.Api/Migrations/[timestamp]_AddFilledQuestionText.cs
1.3 更新 DbContext 欄位映射
檔案: backend/DramaLing.Api/Data/DramaLingDbContext.cs
private void ConfigureFlashcardEntity(ModelBuilder modelBuilder)
{
var flashcardEntity = modelBuilder.Entity<Flashcard>();
// 現有欄位映射...
flashcardEntity.Property(f => f.UserId).HasColumnName("user_id");
flashcardEntity.Property(f => f.PartOfSpeech).HasColumnName("part_of_speech");
flashcardEntity.Property(f => f.ExampleTranslation).HasColumnName("example_translation");
// 新增欄位映射
flashcardEntity.Property(f => f.FilledQuestionText).HasColumnName("filled_question_text");
}
1.4 執行 Migration
命令: dotnet ef database update
✅ 完成標準
- Flashcard 實體包含新欄位
- 資料庫表結構更新完成
- 現有資料保持完整
- 後端編譯成功
Phase 2: 後端服務開發
🎯 目標
實作智能挖空生成服務,支援程式碼挖空和 AI 輔助
📝 具體任務
2.1 建立服務介面
檔案: backend/DramaLing.Api/Services/IBlankGenerationService.cs
public interface IBlankGenerationService
{
Task<string?> GenerateBlankQuestionAsync(string word, string example);
string? TryProgrammaticBlank(string word, string example);
Task<string?> GenerateAIBlankAsync(string word, string example);
bool HasValidBlank(string blankQuestion);
}
2.2 實作挖空服務
檔案: backend/DramaLing.Api/Services/BlankGenerationService.cs
public class BlankGenerationService : IBlankGenerationService
{
private readonly IWordVariationService _wordVariationService;
private readonly IAIProviderManager _aiProviderManager;
private readonly ILogger<BlankGenerationService> _logger;
public BlankGenerationService(
IWordVariationService wordVariationService,
IAIProviderManager aiProviderManager,
ILogger<BlankGenerationService> logger)
{
_wordVariationService = wordVariationService;
_aiProviderManager = aiProviderManager;
_logger = logger;
}
public async Task<string?> GenerateBlankQuestionAsync(string word, string example)
{
if (string.IsNullOrEmpty(word) || string.IsNullOrEmpty(example))
return null;
// Step 1: 嘗試程式碼挖空
var programmaticResult = TryProgrammaticBlank(word, example);
if (!string.IsNullOrEmpty(programmaticResult))
{
_logger.LogInformation("Successfully generated programmatic blank for word: {Word}", word);
return programmaticResult;
}
// Step 2: 程式碼挖空失敗,嘗試 AI 挖空
_logger.LogInformation("Programmatic blank failed for word: {Word}, trying AI blank", word);
var aiResult = await GenerateAIBlankAsync(word, example);
return aiResult;
}
public string? TryProgrammaticBlank(string word, string example)
{
try
{
// 1. 完全匹配
var exactMatch = Regex.Replace(example, $@"\b{Regex.Escape(word)}\b", "____", RegexOptions.IgnoreCase);
if (exactMatch != example)
{
_logger.LogDebug("Exact match blank successful for word: {Word}", word);
return exactMatch;
}
// 2. 常見變形處理
var variations = _wordVariationService.GetCommonVariations(word);
foreach(var variation in variations)
{
var variantMatch = Regex.Replace(example, $@"\b{Regex.Escape(variation)}\b", "____", RegexOptions.IgnoreCase);
if (variantMatch != example)
{
_logger.LogDebug("Variation match blank successful for word: {Word}, variation: {Variation}",
word, variation);
return variantMatch;
}
}
_logger.LogDebug("Programmatic blank failed for word: {Word}", word);
return null; // 挖空失敗
}
catch (Exception ex)
{
_logger.LogError(ex, "Error in programmatic blank for word: {Word}", word);
return null;
}
}
public bool HasValidBlank(string blankQuestion)
{
return !string.IsNullOrEmpty(blankQuestion) && blankQuestion.Contains("____");
}
}
AI 挖空邏輯
public async Task<string?> GenerateAIBlankAsync(string word, string example)
{
try
{
var prompt = $@"
請將以下例句中與詞彙「{word}」相關的詞挖空,用____替代:
詞彙: {word}
例句: {example}
規則:
1. 只挖空與目標詞彙相關的詞(包含變形、時態、複數等)
2. 用____替代被挖空的詞
3. 保持句子其他部分不變
4. 直接返回挖空後的句子,不要額外說明
挖空後的句子:";
_logger.LogInformation("Generating AI blank for word: {Word}, example: {Example}",
word, example);
var result = await _aiProviderManager.GetDefaultProvider()
.GenerateTextAsync(prompt);
// 驗證 AI 回應格式
if (string.IsNullOrEmpty(result) || !result.Contains("____"))
{
_logger.LogWarning("AI generated invalid blank question for word: {Word}", word);
return null;
}
_logger.LogInformation("Successfully generated AI blank for word: {Word}", word);
return result.Trim();
}
catch (Exception ex)
{
_logger.LogError(ex, "Error generating AI blank for word: {Word}", word);
return null;
}
}
2.3 詞彙變形服務
檔案: backend/DramaLing.Api/Services/WordVariationService.cs
public interface IWordVariationService
{
string[] GetCommonVariations(string word);
bool IsVariationOf(string baseWord, string variation);
}
public class WordVariationService : IWordVariationService
{
private readonly ILogger<WordVariationService> _logger;
private readonly Dictionary<string, string[]> CommonVariations = new()
{
["eat"] = ["eats", "ate", "eaten", "eating"],
["go"] = ["goes", "went", "gone", "going"],
["have"] = ["has", "had", "having"],
["be"] = ["am", "is", "are", "was", "were", "been", "being"],
["do"] = ["does", "did", "done", "doing"],
["take"] = ["takes", "took", "taken", "taking"],
["make"] = ["makes", "made", "making"],
["come"] = ["comes", "came", "coming"],
["see"] = ["sees", "saw", "seen", "seeing"],
["get"] = ["gets", "got", "gotten", "getting"],
// ... 更多常見變形
};
public string[] GetCommonVariations(string word)
{
return CommonVariations.TryGetValue(word.ToLower(), out var variations)
? variations
: Array.Empty<string>();
}
public bool IsVariationOf(string baseWord, string variation)
{
var variations = GetCommonVariations(baseWord);
return variations.Contains(variation.ToLower());
}
}
2.4 修改 FlashcardsController
檔案: backend/DramaLing.Api/Controllers/FlashcardsController.cs
GetDueFlashcards 方法強化
[HttpGet("due")]
public async Task<ActionResult> GetDueFlashcards(
[FromQuery] string? date = null,
[FromQuery] int limit = 50)
{
try
{
var userId = GetUserId();
var queryDate = DateTime.TryParse(date, out var parsed) ? parsed : DateTime.Now.Date;
var dueCards = await _spacedRepetitionService.GetDueFlashcardsAsync(userId, queryDate, limit);
// 檢查並生成缺失的挖空題目
foreach(var flashcard in dueCards)
{
if(string.IsNullOrEmpty(flashcard.FilledQuestionText))
{
var blankQuestion = await _blankGenerationService.GenerateBlankQuestionAsync(
flashcard.Word, flashcard.Example);
if(!string.IsNullOrEmpty(blankQuestion))
{
flashcard.FilledQuestionText = blankQuestion;
_context.Entry(flashcard).Property(f => f.FilledQuestionText).IsModified = true;
}
}
}
await _context.SaveChangesAsync();
_logger.LogInformation("Retrieved {Count} due flashcards for user {UserId}",
dueCards.Count, userId);
return Ok(new { success = true, data = dueCards, count = dueCards.Count });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error getting due flashcards");
return StatusCode(500, new { success = false, error = "Failed to get due flashcards" });
}
}
2.5 新增重新生成端點
[HttpPost("{id}/regenerate-blank")]
public async Task<ActionResult> RegenerateBlankQuestion(Guid id)
{
try
{
var userId = GetUserId();
var flashcard = await _context.Flashcards
.FirstOrDefaultAsync(f => f.Id == id && f.UserId == userId);
if (flashcard == null)
{
return NotFound(new { success = false, error = "Flashcard not found" });
}
var blankQuestion = await _blankGenerationService.GenerateBlankQuestionAsync(
flashcard.Word, flashcard.Example);
if (string.IsNullOrEmpty(blankQuestion))
{
return StatusCode(500, new { success = false, error = "Failed to generate blank question" });
}
flashcard.FilledQuestionText = blankQuestion;
flashcard.UpdatedAt = DateTime.UtcNow;
await _context.SaveChangesAsync();
_logger.LogInformation("Regenerated blank question for flashcard {Id}, word: {Word}",
id, flashcard.Word);
return Ok(new { success = true, data = new { filledQuestionText = blankQuestion } });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error regenerating blank question for flashcard {Id}", id);
return StatusCode(500, new { success = false, error = "Failed to regenerate blank question" });
}
}
2.6 服務註冊
檔案: backend/DramaLing.Api/Extensions/ServiceCollectionExtensions.cs
// 在 AddBusinessServices 方法中添加
public static IServiceCollection AddBusinessServices(this IServiceCollection services)
{
// 現有服務...
services.AddScoped<IBlankGenerationService, BlankGenerationService>();
services.AddScoped<IWordVariationService, WordVariationService>();
return services;
}
檔案: backend/DramaLing.Api/Program.cs
// 使用擴展方法
builder.Services.AddBusinessServices();
✅ 完成標準
- BlankGenerationService 服務實作完成
- 常見詞彙變形對應表建立
- AI 挖空整合測試通過
- API 端點功能驗證
- 錯誤處理和日誌完善
Phase 3: 前端組件優化
🎯 目標
簡化 SentenceFillTest 組件,使用後端提供的挖空題目
📝 具體任務
3.1 更新組件 Props 介面
檔案: frontend/components/review/review-tests/SentenceFillTest.tsx
interface SentenceFillTestProps {
word: string
definition: string
example: string // 原始例句
filledQuestionText?: string // 挖空後的題目 (新增)
exampleTranslation: string
pronunciation?: string
difficultyLevel: string
exampleImage?: string
onAnswer: (answer: string) => void
onReportError: () => void
onImageClick?: (image: string) => void
disabled?: boolean
}
3.2 簡化渲染邏輯
// 替換複雜的 renderSentenceWithInput()
const renderFilledSentence = () => {
if (!filledQuestionText) {
// 降級處理:使用當前的程式碼挖空
return renderSentenceWithInput();
}
// 使用後端提供的挖空題目
const parts = filledQuestionText.split('____');
return (
<div className="text-lg text-gray-700 leading-relaxed">
{parts.map((part, index) => (
<span key={index}>
{part}
{index < parts.length - 1 && (
<input
type="text"
value={fillAnswer}
onChange={(e) => setFillAnswer(e.target.value)}
// ... 其他輸入框屬性
/>
)}
</span>
))}
</div>
);
};
3.3 更新頁面使用
檔案: frontend/app/review-design/page.tsx
<SentenceFillTest
word={mockCardData.word}
definition={mockCardData.definition}
example={mockCardData.example}
filledQuestionText={mockCardData.filledQuestionText} // 新增
exampleTranslation={mockCardData.exampleTranslation}
// ... 其他屬性
/>
✅ 完成標準
- SentenceFillTest 組件支援新欄位
- 降級處理機制正常運作
- 前端編譯和類型檢查通過
- review-design 頁面測試正常
Phase 4: 測試與優化
🎯 目標
全面測試智能挖空系統,優化效能和準確性
📝 具體任務
4.1 詞彙變形測試
測試案例:
const testCases = [
{ word: "eat", example: "She ate an apple", expected: "She ____ an apple" },
{ word: "go", example: "He went to school", expected: "He ____ to school" },
{ word: "good", example: "This is better", expected: "This is ____" },
{ word: "child", example: "The children play", expected: "The ____ play" }
];
4.2 AI 挖空品質驗證
- 測試 AI 挖空準確性
- 驗證回應格式正確性
- 檢查異常情況處理
4.3 效能優化
- 批次處理挖空生成
- 資料庫查詢優化
- 快取機制考量
4.4 錯誤處理完善
- AI 服務異常處理
- 網路超時處理
- 降級策略驗證
✅ 完成標準
- 所有測試案例通過
- AI 挖空準確率 > 90%
- API 回應時間 < 2 秒
- 錯誤處理覆蓋率 100%
🚀 部署檢查清單
資料庫
- Migration 執行成功
- 現有資料完整性確認
- 新欄位索引建立(如需要)
後端服務
- BlankGenerationService 註冊成功
- AI 服務整合測試
- API 端點功能驗證
- 日誌記錄完善
前端組件
- SentenceFillTest 組件更新
- TypeScript 類型檢查通過
- 降級處理機制測試
- 用戶介面測試
整合測試
- 端到端填空功能測試
- 各種詞彙變形驗證
- AI 輔助挖空測試
- 效能和穩定性測試
📊 成功指標
功能指標
- ✅ 支援 100% 詞彙變形挖空
- ✅ AI 輔助準確率 > 90%
- ✅ 程式碼挖空成功率 > 80%
技術指標
- ✅ API 回應時間 < 2 秒
- ✅ 前端組件複雜度降低 50%
- ✅ 挖空生成一次處理,多次使用
用戶體驗指標
- ✅ 填空題顯示成功率 100%
- ✅ 智能挖空準確性提升
- ✅ 系統回應速度提升
⚠️ 風險管控
高風險項目
-
AI 服務依賴: Gemini API 可能失敗
- 緩解: 多層回退機制,程式碼挖空 → AI → 手動標記
-
資料庫 Migration: 可能影響現有資料
- 緩解: 充分備份,漸進式部署
-
前端相容性: 新舊版本相容問題
- 緩解: 降級處理邏輯,漸進式替換
監控機制
- 挖空生成成功率監控
- AI 調用耗時和失敗率追蹤
- 使用者填空題完成率分析
📅 時程安排
Week 1
- Day 1-2: Phase 1 (資料庫結構)
- Day 3-5: Phase 2 (後端服務開發)
Week 2
- Day 1-2: Phase 3 (前端組件優化)
- Day 3-4: Phase 4 (測試與優化)
- Day 5: 部署和監控
🔧 開發工具和資源
開發環境
- .NET 8.0 + Entity Framework Core
- Next.js + TypeScript
- SQLite 資料庫
外部服務
- Google Gemini AI API
- 現有的音頻和圖片服務
測試工具
- 單元測試框架
- 整合測試環境
- 效能監控工具
📈 後續擴展
可能的增強功能
- 多語言支援: 支援其他語言的詞彙變形
- 自訂挖空規則: 允許手動調整挖空邏輯
- 挖空難度分級: 根據學習者程度調整挖空複雜度
- 統計分析: 分析挖空成功率和學習效果
技術改進
- 機器學習優化: 基於歷史資料優化挖空準確性
- 快取策略: 實作 Redis 快取提升效能
- 批次處理: 大量詞彙的批次挖空處理
- 監控儀表板: 即時監控系統狀態和效能指標