perf: improve AI prompt cache reuse

This commit is contained in:
shen
2026-09-19 19:21:41 -07:00
parent 7306dc0f85
commit 1a10ca88e0
9 changed files with 318 additions and 124 deletions
@@ -26,6 +26,16 @@ void main() {
),
),
);
expect(
{for (final item in descriptors) item.id: item.promptVersion},
{
'speech-transcription': 1,
'lexicon-explanation': 2,
'dialogue-coach': 3,
'answer-evaluation': 2,
'review-generation': 2,
},
);
});
test(
@@ -0,0 +1,127 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:http/testing.dart';
import 'package:kouyu_english/core/ai_service.dart';
import 'package:kouyu_english/core/models.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test(
'lexicon requests keep a stable system prefix and dynamic JSON last',
() async {
AiService.instance.setFallbackApiKey('test-key');
final captured = <List<dynamic>>[];
Future<void> request(String text) async {
await http.runWithClient(
() => AiService.instance.temporaryDefinition(
provider: AiProviderType.compatible,
endpoint: 'https://api.deepseek.com',
model: 'deepseek-flash',
text: text,
),
() => MockClient((request) async {
final body = jsonDecode(request.body) as Map<String, dynamic>;
captured.add(body['messages'] as List<dynamic>);
return http.Response(
jsonEncode({
'choices': [
{
'message': {'content': '{"definition":"测试"}'},
},
],
}),
200,
headers: {'content-type': 'application/json; charset=utf-8'},
);
}),
);
}
await request('apple');
await request('airport');
expect(captured, hasLength(2));
expect(captured[0], hasLength(2));
expect(captured[0][0]['role'], 'system');
expect(captured[0][0]['content'], captured[1][0]['content']);
expect(captured[0][0]['content'], isNot(contains('apple')));
expect(jsonDecode(captured[0][1]['content'] as String), {
'text': 'apple',
});
expect(jsonDecode(captured[1][1]['content'] as String), {
'text': 'airport',
});
},
);
test(
'dialogue evaluation keeps learner data out of the cached prefix',
() async {
AiService.instance.setFallbackApiKey('test-key');
final captured = <List<dynamic>>[];
Future<void> request({
required String learnerText,
required String turnId,
}) async {
await http.runWithClient(
() => AiService.instance.checkDialogueIntervention(
provider: AiProviderType.compatible,
endpoint: 'https://api.deepseek.com',
model: 'deepseek-flash',
partnerLine: 'How are you?',
taskLabel: 'Say how you feel.',
learnerText: learnerText,
turnId: turnId,
),
() => MockClient((request) async {
final body = jsonDecode(request.body) as Map<String, dynamic>;
captured.add(body['messages'] as List<dynamic>);
return http.Response(
jsonEncode({
'choices': [
{
'message': {
'content': jsonEncode({
'schemaVersion': 'dialogue-intervention-2',
'turnId': turnId,
'accepted': true,
'goalSatisfied': true,
'verdict': 'accepted',
'reasonCode': 'goal-met',
'suggestion': null,
'explanation': '表达自然。',
}),
},
},
],
}),
200,
headers: {'content-type': 'application/json; charset=utf-8'},
);
}),
);
}
await request(learnerText: 'I am fine.', turnId: 'turn-1');
await request(learnerText: 'I feel tired.', turnId: 'turn-2');
final firstSystem = captured[0][0]['content'] as String;
final secondSystem = captured[1][0]['content'] as String;
expect(firstSystem, secondSystem);
expect(firstSystem, isNot(contains('I am fine.')));
expect(firstSystem, isNot(contains('turn-1')));
final firstInput = jsonDecode(captured[0][1]['content'] as String);
final secondInput = jsonDecode(captured[1][1]['content'] as String);
expect(firstInput['learnerText'], 'I am fine.');
expect(firstInput['turnId'], 'turn-1');
expect(secondInput['learnerText'], 'I feel tired.');
expect(secondInput['turnId'], 'turn-2');
},
);
}
+25 -18
View File
@@ -100,13 +100,15 @@ void main() {
);
expect(reply?.reply, 'Where are you from?');
expect(messages, hasLength(3));
expect(messages, hasLength(4));
expect(messages![0]['role'], 'system');
expect(jsonDecode(messages![1]['content'] as String), {
expect(messages![1]['role'], 'user');
expect(messages![1]['content'], startsWith('[ConversationConfig]'));
expect(jsonDecode(messages![2]['content'] as String), {
'reply': "Hi! What's your name?",
});
expect(messages![2]['role'], 'user');
final last = messages![2]['content'] as String;
expect(messages![3]['role'], 'user');
final last = messages![3]['content'] as String;
expect(last, startsWith('My name is Alex.'));
expect(
last,
@@ -114,28 +116,33 @@ void main() {
);
expect(
last,
endsWith('Do not repeat any questions or greetings already asked or answered.'),
endsWith(
'Do not repeat any questions or greetings already asked or answered.',
),
);
expect(messages![0]['content'], isNot(contains('Ask where the learner')));
});
test('dialogue system prompt stays identical across turns', () {
test('dialogue system prompt stays identical across levels and lessons', () {
final words = ['Excuse me.', 'Can you help me?'];
final a = AiService.dialogueSystemPrompt(
level: 'A1',
allowedLanguage: words,
);
final b = AiService.dialogueSystemPrompt(
level: 'A1',
allowedLanguage: words,
);
final a = AiService.dialogueSystemPrompt();
final b = AiService.dialogueSystemPrompt();
expect(a, b);
expect(a, contains('CEFR A1'));
expect(a, contains('Strictly do not repeat any question'));
expect(a, endsWith('Taught language: Excuse me.; Can you help me?'));
expect(a, isNot(contains('Excuse me.')));
final config = AiService.dialogueConversationConfig(
level: 'A1',
allowedLanguage: words,
);
expect(config, contains('A1'));
expect(config, contains('Excuse me.'));
expect(
AiService.dialogueSystemPrompt(level: 'A0', allowedLanguage: const []),
contains('A0 English conversation partner'),
AiService.dialogueConversationConfig(
level: 'A0',
allowedLanguage: const [],
),
contains('simple, common A0 English'),
);
});
}
@@ -57,7 +57,11 @@ void main() {
() => MockClient((request) async {
final body = jsonDecode(request.body) as Map<String, dynamic>;
final messages = body['messages'] as List<dynamic>;
prompts.add((messages.single as Map<String, dynamic>)['content']);
expect(messages, hasLength(2));
expect(messages.first['role'], 'system');
expect(messages.last['role'], 'user');
prompts.add(messages.first['content'] as String);
prompts.add(messages.last['content'] as String);
return http.Response(
jsonEncode({
'choices': [
@@ -73,7 +77,8 @@ void main() {
);
expect(variant, isNotNull);
expect(prompts, hasLength(1));
expect(prompts, hasLength(2));
final fullPrompt = prompts.join('\n');
for (final field in [
'schemaVersion',
'variantId',
@@ -81,10 +86,10 @@ void main() {
'prompt',
'expectedAnswer',
]) {
expect(prompts.single, contains(field));
expect(fullPrompt, contains(field));
}
for (final field in ['stimulus', 'acceptedAnswers', 'forbiddenPhrases']) {
expect(prompts.single, isNot(contains(field)));
expect(fullPrompt, isNot(contains(field)));
}
},
);