问题:一个 Agent 怎么教多门课?

我们有一个 AI 教学助手”海克斯”,基于 OpenClaw Agent 框架搭建,最初只服务于 C 语言课程。现在系统要扩展到多班级多课程——Python、Java、数据结构甚至数学、英语——问题来了:

每个课程的 System Prompt 都不一样。 C 语言需要引导指针和内存管理,Python 需要强调标准库和 pip 生态,数据结构需要聚焦算法复杂度。

最直接的想法:每门课创建一个独立的 Agent。但 OpenClaw 创建 Agent 需要命令行操作,让老师去敲 CLI?这门槛太高了。

我们需要一种方案:老师不需要碰命令行,在 Web 页面就能创建新课、配置 AI 行为。同时 Agent 本身只维护一套,不用新增。


核心思路:Agent 人格与课程知识的三层解耦

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
┌─────────────────────────────────────────────────┐
│ OpenClaw │
│ ┌───────────────────────────────────────────┐ │
│ │ Jarvis Agent(通用教学人格) │ │
│ │ "耐心、引导式教学、授人以渔" │ │
│ │ 不绑定任何学科 │ │
│ └───────────────────────────────────────────┘ │
│ + │
│ ┌───────────────────────────────────────────┐ │
│ │ 应用层 System Prompt(按课程注入) │ │
│ │ 课程A: "C语言助教,擅长指针/内存" │ │
│ │ 课程B: "Python助教,擅长标准库/numpy" │ │
│ │ 课程C: "数据结构助教,擅长算法/图/树" │ │
│ └───────────────────────────────────────────┘ │
│ + │
│ ┌───────────────────────────────────────────┐ │
│ │ MCP 工具回调(检索时注入课程) │ │
│ │ "【系统提示】当前课程:数据结构" │ │
│ └───────────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘

第一层:Jarvis Agent 本身只定义教学风格——耐心、引导式、鼓励为主。不绑定任何具体学科。

第二层:应用层通过 course 表存储每门课的 System Prompt,Web 聊天时从 Redis 会话中提取 courseId,动态渲染 Prompt 后注入请求。

第三层:QQ 群消息绕过 Web 路径时,MCP 工具(searchKnowledge)回调中自动解析 QQ 群号 → 班级 → 课程,在检索结果头部注入课程上下文。


创新点一:预设 Prompt 模板系统

创建新课最麻烦的是写 System Prompt。不是每个老师都擅长 Prompt Engineering。所以我们设计了一套预设模板,老师创建课程时选一个模板,Prompt 自动填充:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
课程名称: [数据结构___________]

预设模板: [数据结构 ▼]
├─ C语言程序设计
├─ Python程序设计
├─ Java程序设计
├─ 数据结构 ← 选中
├─ 通用课程
└─ 自定义(空白)

System Prompt 预览:
┌─────────────────────────────────────────┐
│ 你是教学助手,服务于数据结构课程。 │
│ 1. 涉及数组、链表、栈、队列、树、图 │
│ 等基本数据结构时,先调 searchKnowledge │
│ 2. 涉及排序、查找、递归、动态规划等 │
│ 算法时,同样需要检索知识库 │
│ 3. 班级/学生问题调用对应工具 │
│ ... │
└─────────────────────────────────────────┘
[确认创建]

模板文件是标准的 Java Properties,放在 resources/presets/course-presets.properties

1
2
3
4
5
6
7
8
9
preset.ds.name=数据结构
preset.ds.prompt=你是教学助手,服务于{{courseName}}课程。\
请严格遵循以下工具调用规则:\n\
1. 【必须检索知识库】涉及以下内容时,先调用 searchKnowledge:\n\
- 数组、链表、栈、队列、树、图等基本数据结构\n\
- 排序、查找、递归、动态规划等算法\n\
- 时间/空间复杂度分析\n\
- 任何你记忆不确定的知识性问题\n\
...

{{courseName}} 是占位符,创建时会被替换为实际课程名。老师选模板后也可以手动编辑 Prompt,存入 course 表。

这样做的好处

  • 新增一门课不需要改代码,Web 页面 30 秒搞定
  • 模板覆盖了常见学科(C/Python/Java/数据结构/数学/英语/通用),空白模板支持完全自定义
  • 新增模板只需在 properties 文件加一段,不用动 Java 代码
  • Prompt 存入数据库带 Redis 缓存,读取无性能损耗

创新点二:QQ 群自动路由——无缝注入课程上下文

Web 聊天路径很好办,ChatController 里拿到 userId → classId → courseId,存进 Redis session,OpenClawServiceImpl 构建请求时读取。

但 QQ 群消息是绕过后端的:

1
2
QQ群 → Napcat → OpenClaw Gateway → Jarvis Agent
↑ 绕过 Java 后端

这条路不经过 ChatController,没人调 registerSessionContext,Redis 里没有 session。Jarvis 不知道自己在哪个群里、对应哪门课。

解决方案:借用 MCP 工具回调注入上下文。

当学生在 QQ 群问知识性问题时,Jarvis 遵循 IDENTITY.md 的规则调用 searchKnowledge

1
Jarvis → POST /mcp → searchKnowledge({query, groupId, userId})

我们让 KnowledgeSearchTool 在执行检索前解析上下文:

1
2
3
4
5
6
7
8
// ③ QQ群号 → 班级 → 课程
if (ctx == null) {
ctx = resolveByGroupId((String) arguments.get("groupId"));
}

// resolveByGroupId 内部
ClassInfo cls = classInfoMapper.selectByQqGroupId("875860223");
// → 计算机一班 → courseId=5 → 数据结构

解析出 courseId 后,不只是用 userId 过滤检索结果,还在结果头部注入课程信息

1
2
3
4
5
6
7
8
private String buildCourseHeader(ToolContext ctx) {
Course course = courseService.getById(ctx.getCourseId());
return """
【系统提示】当前对话所属课程:%s。
知识范围:%s。
请以此课程助教身份回答问题。
""".formatted(course.getName(), course.getKnowledgeScope());
}

Jarvis 拿到的检索结果格式:

1
2
3
4
5
6
7
8
9
10
11
【系统提示】当前对话所属课程:数据结构。
知识范围:数组、链表、栈、队列、树、图等基本数据结构。
请以此课程助教身份回答问题。

【C语言知识点总结(1).doc】
数组的定义与初始化、指针与数组的关系...

---

【数据结构教材.md】
链表的基本操作:插入、删除、遍历...

LLM 看到第一行就知道自己在教数据结构了。 后续对话自然延续这个身份,直到 session 过期。

关键设计点group_iduser_id 是通过 Jarvis 的 IDENTITY.md 规则让 LLM 主动传的,不是依赖 OpenClaw 回传——因为 OpenClaw 的 MCP 回调不保证带 session 上下文。这是典型的 Agentic Context Passing 模式。


完整架构图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
┌──────────────────────────────────────────────────────────┐
│ 教师创建课程 │
│ Web页面 → 选预设模板 → course 表 ───→ Redis 缓存 │
│ ↓ │
│ system_prompt + knowledge_scope │
└──────────────────────────────────────────────────────────┘

┌──────────────────┴──────────────────┐
│ │
▼ ▼
┌───────────────┐ ┌───────────────┐
│ Web 聊天路径 │ │ QQ 群路径 │
│ │ │ │
│ ChatController│ │ OpenClaw → LLM │
│ ↓ │ │ ↓ │
│ userId → │ │ LLM 调工具 │
│ classId → │ │ ↓ │
│ courseId │ │ searchKnowledge│
│ ↓ │ │ ↓ │
│ Redis session │ │ groupId → │
│ 存 courseId │ │ ClassInfo → │
│ ↓ │ │ courseId │
│ OpenClawImpl │ │ ↓ │
│ 读 course 表 │ │ 结果头部注入 │
│ 注入 Prompt │ │ 课程上下文 │
└───────────────┘ └───────────────┘


┌─────────────────────┐
│ Jarvis Agent │
│ 通用教学人格 │
│ + 课程专属 Prompt │
└─────────────────────┘

数据库设计

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 课程表:存储课程名 + 专属 Prompt
CREATE TABLE course (
id BIGSERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL, -- "数据结构"
system_prompt TEXT NOT NULL, -- 课程专属 System Prompt
knowledge_scope TEXT, -- 知识范围
teacher_id BIGINT,
created_at TIMESTAMP DEFAULT NOW()
);

-- 班级关联课程
ALTER TABLE class_info ADD COLUMN course_id BIGINT;
ALTER TABLE class_info ADD COLUMN qq_group_id VARCHAR(32);

表结构很简洁:course 存 Prompt,class_info 通过 course_id 关联课程,qq_group_id 实现 QQ 群自动路由。


关键代码片段

动态 Prompt 加载(Web 路径)

1
2
3
4
5
6
7
8
9
10
11
// OpenClawServiceImpl.java
private String resolveSystemPrompt(String sessionId) {
ToolContext ctx = mcpSessionStore.get(sessionId);
if (ctx != null && ctx.getCourseId() != null) {
Course course = courseService.getById(ctx.getCourseId());
if (course != null) {
return course.getSystemPrompt(); // 从 DB 加载
}
}
return DEFAULT_SYSTEM_PROMPT; // 通用兜底
}

QQ 群路由解析(OneBot 路径)

1
2
3
4
5
6
7
8
9
10
// KnowledgeSearchTool.java
private ToolContext resolveByGroupId(String groupId) {
ClassInfo cls = classInfoMapper.selectByQqGroupId(groupId);
if (cls != null) {
Long teacherId = cls.getTeacherId();
Set<Long> kbIds = sharedKbMemberMapper.selectKbIdsByUserId(teacherId);
return new ToolContext(teacherId, kbIds, cls.getCourseId());
}
return null;
}

课程上下文注入(检索结果增强)

1
2
3
4
5
6
7
8
// KnowledgeSearchTool.java
private String buildCourseHeader(ToolContext ctx) {
Course course = courseService.getById(ctx.getCourseId());
return String.format(
"\n【系统提示】当前对话所属课程:%s。知识范围:%s。\n\n",
course.getName(), course.getKnowledgeScope()
);
}

设计原则总结

原则 实践
Agent 不绑定学科 Jarvis IDENTITY.md 只定义教学风格,不写课程
课程配置数据化 Prompt 存入 DB,Web 页面编辑,不写代码
预设模板降门槛 6 个学科模板,老师选模板即用
双路径覆盖 Web 走 Redis session,QQ 走 MCP 工具回调解析
检索即注入 MCP 工具返回结果时自动带课程上下文
Agentic Context Passing LLM 按 IDENTITY.md 规则主动传 groupId/QQ 号

效果验证

数据结构课程群提问

1
2
3
4
学生: 数组和链表有什么区别?
Jarvis: 【读取 searchKnowledge 返回 → 看到 "当前课程:数据结构"】
数组和链表是两种基本的数据结构,它们的核心区别在于...
(自动以数据结构老师身份回答) ✅

Python 课程群提问

1
2
3
4
学生: 列表和元组有什么区别?
Jarvis: 【读取 searchKnowledge 返回 → 看到 "当前课程:Python程序设计"】
Python 中列表和元组的核心区别是可变性...
(自动以 Python 老师身份回答) ✅

Web 聊天路径

1
2
3
4
老师(在数据结构班级下): 帮我写个排序
Jarvis: 【OpenClawServiceImpl 加载数据结构课程的 Prompt】
请问你需要哪种排序算法?冒泡、快排还是归并?
(自动以数据结构老师身份回答) ✅

后续方向

  1. 知识库课程隔离:在 Vector DB 检索时加 courseId 过滤,让不同课程的知识库互相隔离(当前已用 userId 过滤,courseId 过滤可随时叠加)
  2. 学生端自动适配:学生通过 QQ 群提问时自动识别所在课程,无需手动切换
  3. Prompt 版本管理:老师修改 Prompt 后自动回溯历史版本,避免误改导致 AI 行为异常
  4. 跨课程知识点共享:同一知识点(如”算法复杂度”)在多个课程中出现时自动关联

这篇博客的重点:不新建 Agent、不写 CLI、不依赖 OpenClaw 回传 session——通过预设模板 + 动态 Prompt + MCP 工具注入三层解耦,把课程扩展从”运维操作”降级为”填表创建”,让不懂技术的老师也能自己开新课。