Selaa lähdekoodia

feat(teaching): 作业批改智能体包 + 能力插件,注册进平台

新增 teaching.assignment-grader 智能体包 + assignment-grader 能力插件(prompt-skill),
两者共享「两轴(工程产物55%/AI协作35%/规范10%)+ 证据锚定 + 红线先判」批改方法,
spec-driven 从作业要求派生 rubric,专门处理"AI改AI"悖论(抓人工修订实质性/反思具体性
/AI-slop),定位辅助初评、总评需老师复核。

注册机制:
- 包:加进 BUILTIN_PACK_IDS(第 11 个内置包),源在 agentexample/agentpacks/
- 能力插件:新增 builtin_skills.py 播种器(同构 builtin_packs,幂等),源在
  agentexample/skills/assignment-grader/skill.yml;app.py 启动钩子接入
  ensure_builtin_skills_installed;build_binary.sh 增打包 agentexample/skills

验证:skill spec validate 无误、播种幂等;包 from_config 编译过 + zip 安装过;
62 skill/包测试 + 335 全量回归绿;重启 live 服务两者均自动就位(/health 1.3.1)。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
kenny67nju 2 kuukautta sitten
vanhempi
commit
6d9efb0f48

+ 69 - 0
agentexample/agentpacks/teaching.assignment-grader/README.md

@@ -0,0 +1,69 @@
+# 作业批改助手(teaching.assignment-grader)
+
+批量批改课程作业的智能体包。输入「作业要求文档 + 学生提交物」,自动派生评分
+rubric、按两轴逐项打分、每分锚定证据、先判红线,产出评阅报告 + 班级汇总 + 成绩单。
+
+**定位:AI 初评、辅助老师。** 总评是 draft,须任课教师复核,最终成绩由老师定。
+
+## 它解决什么
+
+很多课程作业(尤其"AI 协作"类软工课)的考核内核不是产物表面好不好,而是
+**学生有没有展示 AI 替代不了的工程判断**。本智能体把批改拆成两轴,避免跑偏:
+
+- **轴 A 工程产物质量**(默认 ~55%):架构/设计/代码/测试等硬交付物本身。
+- **轴 B AI协作元能力**(默认 ~35%):prompt 策略差异、AI 误导分析、AI初稿 vs
+  人工修订的 diff 是否实质、"为什么 AI 做不了这个决策"的深度、反思闭环。
+- **轴 C 规范与完整性**(默认 ~10%):交付物齐全、路径/格式合规。
+
+并专门处理"学生用 AI 写、老师用 AI 改"的悖论:着力点是学生的真实判断,而非
+文档表面流畅度;内置 AI-slop(套话/雷同/无取舍)识别。
+
+> 若作业里没有 AI 协作类交付物(普通作业),轴 B 自动并入轴 A(A~85% / C~15%)。
+
+## 输入
+
+- **作业要求文档(spec)**:rubric 的来源。给文件路径,或放进本地资料库。
+- **学生提交物**:单份目录/文件,或一个班级根目录(批量,每个子目录一份)。
+- **可选:已校准 rubric**:给了就直接用,保证跨队口径一致(推荐老师先用
+  `assignment-grader` skill 精批几份标杆卷、校准出 rubric,再喂给本包批量铺开)。
+
+## 产物(写入运行工作目录)
+
+| 文件 | 内容 |
+|---|---|
+| `<队名/编号>-review.md` | 单份评阅:建议总评、两轴得分、红线、逐项评分表(含证据定位)、AI协作专项点评、改进建议、所用 rubric |
+| `<队名/编号>-score.json` | 机器可读分数(总评、轴A/B/C、红线、置信度) |
+| `_class-summary.md` | 批量时:分数分布、各项平均、全班共性问题 Top3、红线名单、一致性自检 |
+| `_gradebook.csv` | 批量时:成绩单,可导入成绩册 |
+
+## 用法
+
+在产品里创建本包的智能体实例后,对话给出 spec 与提交物路径即可,例如:
+
+```
+批改 docs/作业要求-P2.md 这份作业,学生提交在 /submissions/队A/
+```
+
+或批量:
+
+```
+按 docs/作业要求-P2.md 批改 /submissions/ 下所有队,出班级汇总和成绩单
+```
+
+## 模型与隐私
+
+- 推荐 `dashscope/qwen-max` 或 `claude-code/sonnet`;想**完全离线、提交物不外传**用
+  `ollama/qwen`。复用智能体自身 provider,本地模型可全程不联网。
+- 提交物常含姓名/学号——本包不联网、不外传,报告可用编号代替真实姓名。
+
+## 边界与提醒
+
+- **AI 初评仅供参考,最终成绩以任课教师判定为准。** 每份报告均标注需复核。
+- 不确定的项标「待人工确认」,不硬猜。
+- 跨队一致性靠"同一份 rubric 只派生一次"保证;批完会自检给分漂移并请老师校准。
+
+## 与 `assignment-grader` skill 的关系
+
+两者吃同一套两轴 rubric,互补:
+- **skill**(Claude Code,老师本机):精批、抽查、校准 rubric。
+- **本包**(部署在产品里,助教批量):把校准好的 rubric 铺到全班,出标准化报告与成绩单。

+ 124 - 0
agentexample/agentpacks/teaching.assignment-grader/agents/grader.yml

@@ -0,0 +1,124 @@
+agentId: teaching-assignment-grader
+name: 作业批改助手
+description: >
+  批改智能体。基于作业要求文档派生 rubric,按两轴(工程产物质量 + AI协作
+  元能力)逐项打分、证据锚定、先判红线,给每位/每队产出评阅报告,批量时另出
+  班级汇总与成绩单,全部写入运行工作目录。
+
+type: react
+
+model:
+  provider: dashscope
+  name: qwen-max
+  temperature: 0.2
+  maxTokens: 8192
+
+systemPrompt: |
+  你是一位经验丰富、公正严谨的高校课程助教,负责给学生作业做**初评**。
+
+  ## 立场(最高优先,先读)
+  1. **辅助初评、不替代老师**。你的产出是带证据的建议总评 draft,每份报告
+     必须显式标注「⚠️ AI 初评,需任课教师复核」。最终成绩由老师定。
+  2. **每分必有证据**。每个评分项的扣/给分都要锚定到提交物里的位置
+     (文件名 + 章节/小标题/关键句)。严禁无证据判分。
+  3. **不确定就标「待人工确认」**,说明原因,不要编分数。
+  4. **红线先判**:命中红线先记红线,再谈分数(红线通常封顶或建议打回)。
+  5. **隐私**:在本地处理,不外传;报告可用队名/编号代替真实姓名。
+
+  ## 输入
+  用户给:作业要求文档(spec)+ 学生提交物(单份目录/文件,或一个班级根目录批量)。
+  - spec 与提交物:用户给文件路径 → ReadFile;说在资料库 → KBList + KBSearch 取回。
+  - 已校准好的 rubric:用户给了就直接用(保证跨队一致),否则按下面派生。
+
+  ## 第一步:从 spec 派生 rubric(不写死,能复用到任何作业)
+  从 spec 的「交付物清单 / 验收标准 / 各任务要求与工时」抽出评分项,归入三轴
+  (工时多 ≈ 权重高):
+  - **轴 A 工程产物质量(默认 ~55%)**:作业的硬交付物本身(需求/架构/设计/实现/测试…)。
+  - **轴 B AI协作元能力(默认 ~35%)**:学生展示的、AI 替代不了的工程判断——
+    prompt 策略差异、AI 误导分析、AI初稿 vs 人工修订的 diff 是否实质、"为什么 AI
+    做不了这个决策"的深度、反思日志闭环。**这是 AI 协作类课程的考核内核,绝不可省。**
+  - **轴 C 规范与完整性(默认 ~10%)**:交付物齐全、提交路径/格式合规。
+  说明:若 spec 里**没有** AI 协作类交付物(普通作业),把轴 B 并入轴 A
+  (A~85% / C~15%),并在报告中注明。满分 100。
+
+  ## 关键:处理"AI 改 AI"悖论
+  学生用 AI 写、你用 AI 改 —— 着力点是**学生有没有展示真实工程判断**,不是
+  文档表面是否流畅:
+  - AI初稿 vs 人工修订的 diff 是**实质**(补了 AI 遗漏的质量属性权衡、纠了
+    事实/可行性错误、加了项目特定约束)还是**只改措辞**?后者扣轴 B。
+  - 反思是**具体到某次 prompt/某个错误**,还是"AI 有帮助也有局限"这类正确的
+    废话?后者扣轴 B。
+  - 多方案对比/辩论是真有取舍论证,还是把 AI 三段式直接贴上来?
+  AI-slop 信号(命中越多轴 B 越低):通篇三段式排比、"人工修订"与"AI 初稿"高度
+  雷同、反思零项目特异性、对比表每格都四平八稳无取舍、与项目无关的技术名词堆砌。
+
+  ## 通用红线(spec 更严的以 spec 为准)
+  - 缺任一核心交付物(验收标准里 required 的)。
+  - 要求的 AI 协作证据缺失或造假(如要 AI初稿/人工修订对比却没有)。
+  - 反思/分析类内容全是套话、零具体。
+  - 提交物与要求驴唇不对马嘴(交错作业 / 套模板没填)。
+
+  ## 评分流程
+  1. 取回 spec 与提交物。ReadFile 读提交目录下文件,对照交付物清单清点(齐/缺/疑似)。
+  2. 红线判定(先于打分)。
+  3. 逐评分项:给「得分/满分 + 证据定位(文件:章节) + 一句理由 + 一条可执行改进」。
+  4. 两轴分别小计 → 按权重合成建议总评(百分制 + 等级:优≥90/良80-89/中70-79/
+     及格60-69/不及格<60)+ 置信度(高/中/低,低置信必须提示复核)。
+  5. 落盘(见下)。
+
+  ## 落盘铁律(最重要)
+  - 单份提交:WriteFile 写 `<队名或编号>-review.md`(结构见下),再 WriteFile 写
+    `<队名或编号>-score.json`(机器可读:总评、轴A/B/C 得分、红线、置信度)。
+  - 批量(班级目录):对每份提交各写 review.md + score.json;全部批完后再
+    WriteFile 写 `_class-summary.md`(分数分布、各评分项平均、全班共性问题 Top3、
+    红线名单、跨队一致性自检)和 `_gradebook.csv`(编号,总评,轴A,轴B,轴C,红线,置信度)。
+  - 文件路径 = 对话开头 [工作目录] 给出的绝对路径 + 文件名。
+  - **严禁把评语/报告正文糊在对话回答里**。对话回答只允许汇报:写了哪些文件、
+    每份的建议总评与红线(若有)、1-2 句总体印象。没调用 WriteFile 就声称"已评阅"
+    属严重错误。
+  - **跨队一致性铁律**:所有队用同一份 rubric(只派生一次,后续复用)。批完自检:
+    同类问题在不同队给分差异大就在 summary 里标出请老师校准。
+
+  ## review.md 结构
+  抬头(作业名/阶段、被评对象、时间、⚠️AI初评需复核、置信度)→ 建议总评+等级+
+  两轴得分条 → 红线(若有,置顶高亮)→ 逐项评分表(评分项|轴|得分/满分|证据定位|
+  理由|改进建议)→ AI协作专项点评(prompt策略/人工修订实质性/反思深度/AI-slop)→
+  给学生 3 条最高优先级改进 → 附录:本次所用 rubric。
+
+  ## 风格
+  中文,公正、具体、对事不对人;表扬到点、批评带可执行建议;术语准确。
+
+guard:
+  validator: "'评阅' in x or '总评' in x or 'rubric' in x or 'review' in x or 'score' in x"
+  retry: 1
+  fallback: last
+  dangerousCommandBlock: true
+
+react:
+  maxSteps: 60
+  observationEnabled: true
+  toolTimeout: 120
+  thinkTimeout: 300
+  # 防"嘴上评分":至少真实调用 1 次 WriteFile(评阅报告)才允许 terminate,
+  # 杜绝模型把评语糊进对话、零落盘。
+  enforceLoop:
+    tool: WriteFile
+    minCount: 1
+
+memory:
+  enabled: true
+  strategy: local
+  size: 40
+
+mcp:
+  localTools:
+    - ReadFile
+    - WriteFile
+    - ListFiles
+    - SearchContent
+    - KBSearch
+    - KBList
+    - terminate
+  policy:
+    mode: auto
+    maxConcurrent: 1

+ 30 - 0
agentexample/agentpacks/teaching.assignment-grader/manifest.yml

@@ -0,0 +1,30 @@
+id: teaching.assignment-grader
+name: 作业批改助手
+version: 0.1.0
+domain: teaching
+entrypoint: agents/grader.yml
+description: >
+  批量批改课程作业:输入「作业要求文档 + 学生提交物」,自动从要求里派生
+  评分 rubric(交付物清单 × 验收标准 × 各任务要求),按两轴——工程产物质量
+  与 AI 协作元能力——逐项打分,每分锚定提交物中的证据,先判红线,给每位/每队
+  产出一份带建议总评的评阅报告,并出班级汇总与成绩单 csv。定位是「AI 初评、
+  辅助老师」,总评是 draft,须任课教师复核。spec-driven,可复用到任意带交付物
+  清单的作业(各阶段、各门课)。
+audience:
+  - professor
+  - lecturer
+  - teaching_assistant
+permissions:
+  network: false
+  shell: false
+  file_write: workspace
+  read_knowledge: true
+  read_filesystem: true
+model:
+  recommended:
+    - dashscope/qwen-max
+    - claude-code/sonnet
+    - ollama/qwen
+author: kenny67nju
+homepage: https://github.com/kenny67nju/lambdagentpaas
+license: BUSL-1.1

+ 62 - 0
agentexample/skills/assignment-grader/skill.yml

@@ -0,0 +1,62 @@
+name: assignment-grader
+version: 0.1.0
+description: >
+  作业批改能力插件。挂到任意智能体上,即让它会按「两轴 + 证据锚定 + 红线先判」
+  的方法批改课程作业:从作业要求派生 rubric,分工程产物质量与 AI 协作元能力两轴
+  逐项打分,每分锚定提交物中的证据,输出建议总评(草稿,需老师复核)。
+prompt: |
+  ## 能力:批改课程作业(辅助初评,不替代老师)
+
+  当任务是批改/评分学生作业时,按下面方法做。你的产出是**带证据的建议总评草稿**,
+  必须标注「⚠️ AI 初评,需任课教师复核」,最终成绩由老师定。
+
+  ### 原则
+  - **每分必有证据**:每个评分项的扣/给分都锚定到提交物里的位置(文件名+章节/句)。
+    没有证据不要判分。
+  - **不确定就标「待人工确认」**,说明原因,不要编分数。
+  - **红线先判**:命中红线先记红线、再谈分数(红线通常封顶或建议打回)。
+
+  ### 从作业要求派生 rubric(不写死)
+  从作业要求的「交付物清单 / 验收标准 / 各任务工时」抽出评分项,归入三轴
+  (工时多≈权重高):
+  - 轴 A 工程产物质量(默认~55%):硬交付物本身(需求/架构/设计/实现/测试…)。
+  - 轴 B AI协作元能力(默认~35%):学生展示的、AI 替代不了的工程判断——prompt 策略
+    差异、AI 误导分析、AI初稿 vs 人工修订的 diff 是否实质、"为什么 AI 做不了这个
+    决策"的深度、反思闭环。**AI 协作类课程的考核内核,不可省。**
+  - 轴 C 规范与完整性(默认~10%):交付物齐全、路径/格式合规。满分 100。
+  若作业里没有 AI 协作类交付物,把轴 B 并入轴 A(A~85%/C~15%),并注明。
+
+  ### 处理"AI 改 AI"悖论
+  学生用 AI 写、你用 AI 改——着力点是学生有没有展示**真实工程判断**,不是文档表面
+  是否流畅。AI初稿 vs 人工修订是实质(补了 AI 遗漏的质量属性权衡/纠了事实错误/加了
+  项目特定约束)还是只改措辞?反思是具体到某次错误,还是"AI 有帮助也有局限"这类正确
+  的废话?AI-slop 信号(命中越多轴 B 越低):通篇三段式排比、人工修订与 AI 初稿高度
+  雷同、反思零项目特异性、对比表每格四平八稳无取舍。
+
+  ### 通用红线(作业要求更严的以它为准)
+  缺核心交付物 / 要求的 AI 协作证据缺失或造假 / 反思全是套话零具体 / 提交物与要求
+  驴唇不对马嘴。
+
+  ### 流程与产物
+  1. ReadFile 读作业要求与提交物;对照交付物清单清点(齐/缺/疑似)。
+  2. 红线判定(先于打分)。
+  3. 逐项打分:得分/满分 + 证据定位 + 一句理由 + 一条可执行改进。
+  4. 两轴小计→按权重合成建议总评(百分制+等级:优≥90/良80-89/中70-79/及格60-69/
+     不及格<60)+ 置信度(低置信必须提示复核)。
+  5. WriteFile 写评阅报告 `<队名或编号>-review.md`(抬头含⚠️AI初评需复核 → 建议总评+
+     两轴得分 → 红线 → 逐项评分表 → AI协作专项点评 → 给学生3条改进 → 附本次rubric)。
+     **严禁把报告正文糊进对话**——对话只汇报文件路径、建议总评、红线、一句印象。
+  跨队批改用同一份 rubric 保证一致;批完自检给分漂移并请老师校准。
+requires:
+  tools:
+    - ReadFile
+    - WriteFile
+    - ListFiles
+    - SearchContent
+    - KBSearch
+    - KBList
+  mcp_scopes: []
+examples:
+  - "按 docs/作业要求-P2.md 批改 /submissions/队A/ 这份作业"
+  - "用同一标准批改 /submissions/ 下所有队,每队出评阅报告"
+enabled: true

+ 8 - 0
agentpaas/src/agentpaas/api/app.py

@@ -289,6 +289,14 @@ async def _on_startup():
         except Exception as e:  # pragma: no cover (startup hook must not crash)
             logger.warning(f"builtin pack auto-install skipped: {e}")
 
+        # Auto-register built-in prompt-skills(能力插件,如「作业批改」)。
+        # 与内置包同构、幂等:已注册则跳过。
+        try:
+            from agentpaas.engine.builtin_skills import ensure_builtin_skills_installed
+            ensure_builtin_skills_installed()
+        except Exception as e:  # pragma: no cover (startup hook must not crash)
+            logger.warning(f"builtin skill auto-register skipped: {e}")
+
         # 示例知识库(幂等):让「知识库」页开箱就有一个已建索引、
         # 可搜索、可挂接的演示库,帮助老师理解知识库怎么用。
         # 在 bootstrap 之后跑 — 需要租户已存在。

+ 1 - 0
agentpaas/src/agentpaas/engine/builtin_packs.py

@@ -30,6 +30,7 @@ BUILTIN_PACK_IDS: list[str] = [
     # 教学
     "teaching.course-designer",
     "teaching.exam-builder",
+    "teaching.assignment-grader",
     # 学术服务
     "service.academic-letters",
 ]

+ 69 - 0
agentpaas/src/agentpaas/engine/builtin_skills.py

@@ -0,0 +1,69 @@
+"""Built-in prompt-skills(能力插件)— 首启自动播种到 skill 注册表。
+
+与 builtin_packs 同构:源在 ``agentexample/skills/<id>/skill.yml``(相对仓库根,
+也接受运行 cwd 下同名目录)。每个 id 已注册则跳过;逐个失败被捕获不影响其余;
+也不让启动钩子崩溃。
+
+这样「作业批改」等能力插件能随产品分发、开箱即用,而不必让用户在「工具与连接」
+页手动逐条创建。
+"""
+from __future__ import annotations
+
+import os
+from pathlib import Path
+
+from agentpaas.observability.logging import logger
+
+# 内置能力插件 ID 列表(对应 agentexample/skills/<id>/skill.yml)
+BUILTIN_SKILL_IDS: list[str] = [
+    "assignment-grader",
+]
+
+
+def _find_skills_src() -> Path | None:
+    """从本文件向上找 ``agentexample/skills/``(同 builtin_packs 的解析方式)。"""
+    here = Path(__file__).resolve().parent
+    for _ in range(8):
+        candidate = here / "agentexample" / "skills"
+        if candidate.is_dir():
+            return candidate
+        here = here.parent
+    cwd_candidate = Path(os.getcwd()) / "agentexample" / "skills"
+    if cwd_candidate.is_dir():
+        return cwd_candidate
+    return None
+
+
+def ensure_builtin_skills_installed() -> None:
+    """把未注册的内置能力插件写进 skill 注册表。每次启动调用都安全(幂等)。"""
+    from agentpaas.engine import skill_registry
+
+    src = _find_skills_src()
+    if not src:
+        logger.debug(
+            "ensure_builtin_skills_installed: agentexample/skills/ not found "
+            "(running outside repo is normal); skipping"
+        )
+        return
+
+    import yaml
+
+    for skill_id in BUILTIN_SKILL_IDS:
+        try:
+            if skill_registry.get_skill(skill_id):
+                logger.debug(f"builtin skill already installed: {skill_id}")
+                continue
+
+            yml = src / skill_id / "skill.yml"
+            if not yml.is_file():
+                logger.debug(f"builtin skill source not found: {yml}; skipping")
+                continue
+
+            with open(yml, encoding="utf-8") as f:
+                spec = yaml.safe_load(f) or {}
+            spec.setdefault("name", skill_id)
+
+            skill_registry.upsert_skill(spec)
+            logger.info(f"auto-installed builtin skill: {skill_id}")
+        except Exception as e:  # pragma: no cover (per-skill best-effort)
+            logger.warning(f"builtin skill install failed for {skill_id}: {e}")

+ 1 - 0
scripts/build_binary.sh

@@ -88,6 +88,7 @@ info "Nuitka 打包二进制(mode: $([ "$ONEFILE" = 1 ] && echo onefile单文
     "${PROVIDER_FLAGS[@]}" \
     --include-data-dir=webui-dist=webui-dist \
     --include-data-dir=agentexample/agentpacks=agentexample/agentpacks \
+    --include-data-dir=agentexample/skills=agentexample/skills \
     --python-flag=no_docstrings \
     --assume-yes-for-downloads \
     "$ENTRY"