问题:一个 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
| if (ctx == null) { ctx = resolveByGroupId((String) arguments.get("groupId")); }
ClassInfo cls = classInfoMapper.selectByQqGroupId("875860223");
|
解析出 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_id 和 user_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
| CREATE TABLE course ( id BIGSERIAL PRIMARY KEY, name VARCHAR(100) NOT NULL, system_prompt TEXT NOT NULL, 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
| 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(); } } return DEFAULT_SYSTEM_PROMPT; }
|
QQ 群路由解析(OneBot 路径)
1 2 3 4 5 6 7 8 9 10
| 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
| 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】 请问你需要哪种排序算法?冒泡、快排还是归并? (自动以数据结构老师身份回答) ✅
|
后续方向
- 知识库课程隔离:在 Vector DB 检索时加
courseId 过滤,让不同课程的知识库互相隔离(当前已用 userId 过滤,courseId 过滤可随时叠加)
- 学生端自动适配:学生通过 QQ 群提问时自动识别所在课程,无需手动切换
- Prompt 版本管理:老师修改 Prompt 后自动回溯历史版本,避免误改导致 AI 行为异常
- 跨课程知识点共享:同一知识点(如”算法复杂度”)在多个课程中出现时自动关联
这篇博客的重点:不新建 Agent、不写 CLI、不依赖 OpenClaw 回传 session——通过预设模板 + 动态 Prompt + MCP 工具注入三层解耦,把课程扩展从”运维操作”降级为”填表创建”,让不懂技术的老师也能自己开新课。