ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

AI Agent Harness Engineering 教育场景应用:个性化辅导与智能教学助手搭建

AI Agent Harness Engineering 教育场景应用:个性化辅导与智能教学助手搭建 1. 教育场景下 AI Agent Harness Engineering 到底解决什么问题AI Agent Harness Engineering 这个词听起来很重但落到教育场景里它要解决的事情其实特别具体让一个智能教学助手能记住学生上次卡在哪、知道下一步该推什么题、还能在对话里像真人老师一样一步步引导而不是每次问答都从零开始。它适合谁适合想用大模型做个性化辅导产品的前后端工程师、教研产品经理以及想给自己孩子搭一个“不会不耐烦的陪练”的技术家长。我见过太多“AI 家教”Demo 死在同一处单轮问答很惊艳多轮一聊就露馅。学生问“为什么一次函数图像是直线”模型答得头头是道学生接着问“那我上次错的那道斜率题呢”模型立刻失忆。问题不在模型能力而在缺少一层编排Harness——把学生画像、知识图谱、工具调用、会话记忆串成一条稳定链路。Harness Engineering 的核心工作就是设计这层“骨架”定义 Agent 能调用哪些工具、状态怎么在轮次间传递、什么条件下触发检索或出题、失败时如何降级。这篇会带你从零搭一个可运行的最小系统先给settings.json和config.toml两套配置骨架再配一条统一 Key 通道然后演示多轮辅导对话、学情记忆写入与工具调用的完整配置最后本地起服务跑通一次“提问—引导—出题—记录”的辅导链路。全程可复制不需要你先有一整套教研中台。2. TaoToken 前置统一 Key 通道与项目骨架2.1 为什么教育 Agent 需要一个统一 Key 通道教育场景的 Agent 往往要同时干几件事主对话用一个大模型、出题校验用另一个、学情摘要可能还要一个小模型。如果每个模块各自配 Key、各自写 base_url后面换模型或加限流会非常痛苦。统一 Key 通道的意思是所有模型请求都走同一个入口由这层统一做鉴权、路由和日志。TaoToken 的 API 入口是https://taotoken.net/api兼容 OpenAI 风格的调用方式把它作为统一通道你的 Agent 代码里只需要维护一份配置。先拿到访问凭证打开https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite创建 API Key。建议按环境分 Key比如dev-tutor、prod-tutor方便出问题时快速定位和吊销。2.2 settings.json 骨架面向 JSON 配置的项目如果你的项目用 JSON 配置Node/前端工具链常见可以这样组织。核心是把模型通道、Agent 行为、学情存储三块分开避免后期改一个参数牵动全身。{ llm: { provider: taotoken, base_url: https://taotoken.net/api, api_key_env: TAOTOKEN_API_KEY, default_model: gpt-4o-mini, fallback_model: gpt-3.5-turbo, timeout_ms: 30000, max_retries: 2 }, agent: { name: k12-math-tutor, max_turns: 20, system_prompt_file: ./prompts/tutor_system.md, enable_tool_call: true, tools: [query_knowledge_graph, get_student_profile, recommend_exercise, save_learning_record], memory: { type: session_plus_profile, session_ttl_minutes: 120, profile_store: sqlite://./data/profile.db } }, guardrail: { reject_off_topic: true, max_answer_length: 800, require_step_by_step: true } }这里几个字段值得说明。api_key_env指向环境变量而不是把 Key 写进文件这是底线。fallback_model用于主模型超时或限流时降级教育场景里宁可答得朴素一点也别让学生干等。memory.type设为session_plus_profile意思是短期会话记忆和长期学情画像分开存会话记忆管“这轮聊到哪了”学情画像管“这个学生整体掌握情况”。2.3 config.toml 骨架面向 Python 服务的配置Python 侧更习惯 TOML结构可以更贴近代码里的 dataclass。[llm] provider taotoken base_url https://taotoken.net/api api_key_env TAOTOKEN_API_KEY default_model gpt-4o-mini fallback_model gpt-3.5-turbo timeout_ms 30000 max_retries 2 [agent] name k12-math-tutor max_turns 20 system_prompt_file ./prompts/tutor_system.md enable_tool_call true tools [ query_knowledge_graph, get_student_profile, recommend_exercise, save_learning_record ] [agent.memory] type session_plus_profile session_ttl_minutes 120 profile_store sqlite://./data/profile.db [guardrail] reject_off_topic true max_answer_length 800 require_step_by_step true两套配置字段一一对应团队里谁用哪套语言都不影响协作。真正要统一的是base_url和api_key_env这两个值它们决定了所有请求是否走同一条通道。2.4 环境变量与依赖安装无论用哪套配置Key 都通过环境变量注入export TAOTOKEN_API_KEYsk-你的实际KeyPython 侧安装依赖pip install openai fastapi uvicorn pydantic sqlalchemyNode 侧npm install openai express better-sqlite3到这里前置就绪。接下来进入可复制配置环节把 Agent 的“大脑”和“手脚”接起来。3. 可复制配置多轮辅导、学情记忆与工具调用3.1 系统提示词让模型像老师而不是答题机Harness Engineering 里提示词是 Agent 的行为契约。教育场景的系统提示词要明确三件事不直接给答案、先判断学生卡点、每轮结束给一个可执行的小任务。# prompts/tutor_system.md 你是一位耐心、善于引导的 K12 数学辅导老师。你的目标不是给出答案而是帮助学生自己想到答案。 ## 行为准则 1. 学生提问后先用一句话确认你理解了他的问题。 2. 不要直接给出最终答案。先问一个诊断性问题判断他卡在概念、计算还是审题。 3. 如果学生连续两次答不上来降低难度回到前置知识点。 4. 每轮回复结尾给学生一个小任务一道口算、一个判断题、或复述一个概念。 5. 涉及具体知识点时调用 query_knowledge_graph 工具确认前置依赖不要凭记忆编造。 6. 学生答对后调用 save_learning_record 记录本次掌握情况。 ## 语气 像朋友一样不用“同学你好”这类套话。可以适当用生活例子比如用“打车计费”解释一次函数。这份提示词的关键在“调用工具确认前置依赖”和“记录掌握情况”两条它们把模型从纯文本生成器变成了会操作外部状态的 Agent。3.2 工具定义知识图谱、学情画像、出题与记录工具调用是 Harness 的“手脚”。下面用 Python 定义四个核心工具注册到 Agent 的工具表里。# tools.py import json import sqlite3 from datetime import datetime DB_PATH ./data/profile.db def query_knowledge_graph(node_name: str) - dict: 查询知识点的前置依赖与难度。 # 实际项目接 Neo4j 或图数据库这里用字典模拟 graph { 一次函数: {prerequisites: [函数定义, 定义域], difficulty: 0.5}, 二次函数: {prerequisites: [一次函数, 函数图像], difficulty: 0.7}, 函数定义: {prerequisites: [], difficulty: 0.3}, } return graph.get(node_name, {prerequisites: [], difficulty: 0.5}) def get_student_profile(student_id: str) - dict: 读取学生画像掌握程度、学习风格、错误模式。 conn sqlite3.connect(DB_PATH) cur conn.cursor() cur.execute( SELECT node_name, mastery, error_pattern FROM profile WHERE student_id?, (student_id,), ) rows cur.fetchall() conn.close() return { student_id: student_id, mastery: {r[0]: r[1] for r in rows}, error_patterns: [r[2] for r in rows if r[2]], } def recommend_exercise(node_name: str, difficulty: float) - dict: 按知识点和难度推荐一道练习题。 bank { 一次函数: [ {q: 已知 y2x1当 x3 时 y 等于多少, difficulty: 0.3}, {q: 一次函数 ykxb 经过点(1,3)和(2,5)求 k 和 b。, difficulty: 0.6}, ] } candidates bank.get(node_name, []) if not candidates: return {question: None, message: 该知识点暂无题目} closest min(candidates, keylambda c: abs(c[difficulty] - difficulty)) return {question: closest[q], difficulty: closest[difficulty]} def save_learning_record(student_id: str, node_name: str, mastery: float, error_pattern: str ) - dict: 写入或更新学情记录。 conn sqlite3.connect(DB_PATH) cur conn.cursor() cur.execute( INSERT INTO profile (student_id, node_name, mastery, error_pattern, updated_at) VALUES (?, ?, ?, ?, ?) ON CONFLICT(student_id, node_name) DO UPDATE SET mastery?, error_pattern?, updated_at?, (student_id, node_name, mastery, error_pattern, datetime.now().isoformat(), mastery, error_pattern, datetime.now().isoformat()), ) conn.commit() conn.close() return {status: ok, node: node_name, mastery: mastery}初始化数据库表# init_db.py import sqlite3, os os.makedirs(./data, exist_okTrue) conn sqlite3.connect(./data/profile.db) conn.execute( CREATE TABLE IF NOT EXISTS profile ( student_id TEXT, node_name TEXT, mastery REAL, error_pattern TEXT, updated_at TEXT, PRIMARY KEY (student_id, node_name) ) ) conn.commit() conn.close() print(profile.db 初始化完成)3.3 多轮辅导对话主循环把配置、提示词、工具串起来就是 Agent 的主循环。这里用 OpenAI 兼容的调用方式base_url 指向统一通道。# tutor_agent.py import os, json from openai import OpenAI from tools import query_knowledge_graph, get_student_profile, recommend_exercise, save_learning_record client OpenAI( api_keyos.environ[TAOTOKEN_API_KEY], base_urlhttps://taotoken.net/api, ) TOOLS [ { type: function, function: { name: query_knowledge_graph, description: 查询知识点的前置依赖和难度, parameters: { type: object, properties: {node_name: {type: string}}, required: [node_name], }, }, }, { type: function, function: { name: get_student_profile, description: 读取学生学情画像, parameters: { type: object, properties: {student_id: {type: string}}, required: [student_id], }, }, }, { type: function, function: { name: recommend_exercise, description: 按知识点和难度推荐练习题, parameters: { type: object, properties: { node_name: {type: string}, difficulty: {type: number}, }, required: [node_name, difficulty], }, }, }, { type: function, function: { name: save_learning_record, description: 记录学生掌握情况, parameters: { type: object, properties: { student_id: {type: string}, node_name: {type: string}, mastery: {type: number}, error_pattern: {type: string}, }, required: [student_id, node_name, mastery], }, }, }, ] TOOL_MAP { query_knowledge_graph: query_knowledge_graph, get_student_profile: get_student_profile, recommend_exercise: recommend_exercise, save_learning_record: save_learning_record, } def load_system_prompt(path./prompts/tutor_system.md): with open(path, r, encodingutf-8) as f: return f.read() def run_tutor_turn(student_id: str, history: list, user_input: str) - tuple: 执行一轮辅导返回(回复文本, 更新后的history)。 history.append({role: user, content: user_input}) messages [{role: system, content: load_system_prompt()}] history for _ in range(5): # 最多5次工具调用循环 resp client.chat.completions.create( modelgpt-4o-mini, messagesmessages, toolsTOOLS, tool_choiceauto, temperature0.4, ) msg resp.choices[0].message if not msg.tool_calls: history.append({role: assistant, content: msg.content}) return msg.content, history messages.append(msg) for call in msg.tool_calls: fn TOOL_MAP.get(call.function.name) args json.loads(call.function.arguments) if call.function.name get_student_profile: args[student_id] student_id if call.function.name save_learning_record: args[student_id] student_id result fn(**args) if fn else {error: unknown tool} messages.append({ role: tool, tool_call_id: call.id, content: json.dumps(result, ensure_asciiFalse), }) fallback 我们先停一下你把这轮的想法用自己的话复述一遍我再接着帮你。 history.append({role: assistant, content: fallback}) return fallback, history这段代码里有两个 Harness 设计点值得注意。第一工具调用放在for _ in range(5)循环里防止模型陷入无限调用。第二student_id由服务端注入而不是让模型填避免模型编造或串号这是教育场景数据隔离的基本要求。3.4 用 FastAPI 暴露一个辅导接口# server.py from fastapi import FastAPI from pydantic import BaseModel from tutor_agent import run_tutor_turn app FastAPI() SESSIONS {} class ChatRequest(BaseModel): student_id: str message: str app.post(/tutor/chat) def chat(req: ChatRequest): history SESSIONS.get(req.student_id, []) reply, history run_tutor_turn(req.student_id, history, req.message) SESSIONS[req.student_id] history[-20:] # 保留最近20条 return {reply: reply, turns: len(history)} app.get(/health) def health(): return {status: ok}启动python init_db.py uvicorn server:app --host 0.0.0.0 --port 80004. 验证请求跑通一次完整辅导链路4.1 健康检查与首轮提问先确认服务活着curl http://localhost:8000/health返回{status:ok}即可。然后模拟一个学生提问curl -X POST http://localhost:8000/tutor/chat \ -H Content-Type: application/json \ -d {student_id:stu_001,message:老师一次函数 y2x1 的图像为什么是直线}预期回复不会直接讲“因为两点确定一条直线”而是先确认理解再抛一个诊断问题比如“你先说说如果 x 取 0 和 1y 分别是多少把这两个点画出来看看”。这就是引导式交互生效的标志。4.2 验证工具调用是否真的发生在run_tutor_turn里临时加一行日志或在工具函数里打印调用记录。正常链路下第一轮可能触发query_knowledge_graph(一次函数)拿到前置依赖[函数定义, 定义域]。如果学生画像里“函数定义”掌握度只有 0.4Agent 应该先补前置概念而不是硬讲一次函数。你可以用第二轮追问来验证记忆curl -X POST http://localhost:8000/tutor/chat \ -H Content-Type: application/json \ -d {student_id:stu_001,message:x0 时 y1x1 时 y3然后呢}如果 Agent 能接着上一轮的点继续引导说明会话记忆生效。再问一句“我上次斜率那块总错”看它是否调用get_student_profile并引用历史错误模式。4.3 验证学情写入让学生答对一道题后检查数据库sqlite3 ./data/profile.db SELECT * FROM profile WHERE student_idstu_001;如果看到一次函数的 mastery 被更新说明save_learning_record工具被正确触发。这一步是很多 Demo 忽略的没有写回个性化就是假的。4.4 验证降级路径把TAOTOKEN_API_KEY临时改错再发一次请求。理想行为是主模型失败后走fallback_model或返回一句友好的“我这会儿有点卡你先自己算一下 x2 时 y 是多少”。如果直接抛 500 堆栈说明 Harness 的容错层还没做需要补 try/except 和降级逻辑。5. 本篇常见错排查5.1 报错401 Unauthorized或invalid api key最常见原因是环境变量没生效。检查echo $TAOTOKEN_API_KEY是否有值以及代码里读的是不是同一个变量名。另一个坑是把 Key 写进了settings.json但代码读的是环境变量两边不一致。统一走api_key_env能避免这类问题。5.2 工具调用不触发模型一直纯文本回复先确认tools参数传了、tool_choice是auto。如果模型仍然不调用通常是系统提示词里没明确要求。在提示词里加一句“涉及知识点时必须先调用 query_knowledge_graph 确认前置依赖”触发率会明显上升。另外部分模型对工具描述敏感description写得太模糊也会导致不调用。5.3 多轮对话“失忆”第二轮不记得第一轮检查SESSIONS是否按student_id正确存取以及history是否在每轮后更新。常见错误是每轮都新建空 history。另一个隐蔽问题是 history 太长被截断把关键上下文丢了。保留最近 20 条是折中重要学情应该落到 profile 而不是只靠会话记忆。5.4 学情写入重复或覆盖错误save_learning_record用了ON CONFLICT DO UPDATE如果主键设计不对会插重复行。确认profile表的主键是(student_id, node_name)。另外mastery 的更新策略要定义清楚是覆盖、取平均还是按答题次数加权。教育场景建议用滑动平均避免一次偶然答对就判定掌握。5.5 请求超时或响应很慢教育 Agent 一轮可能触发多次工具调用加多次模型请求总耗时容易超 30 秒。优化方向把不依赖模型的工具如查画像、查图谱并行执行给模型请求设timeout_ms对同一学生的画像做短时缓存。如果还是慢考虑把出题校验拆到异步任务主对话先返回引导语。5.6 模型编造知识点前置关系这是幻觉的典型表现。Harness 层的对策是凡涉及知识结构强制走query_knowledge_graph并在提示词里写明“不要凭记忆编造前置依赖”。如果图谱里查不到让 Agent 明确说“这个知识点我暂时没有把握我们换个角度”而不是硬编。6. 继续搭建从最小系统到可用教学助手跑通上面这条链路后你已经有了一个能记住学情、会调用工具、能多轮引导的辅导 Agent 骨架。接下来可以按需扩展把 SQLite 换成 Postgres 支持多教师共用给recommend_exercise接真实题库用https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi_keysutm_campaignrewrite里不同环境的 Key 做灰度发布。如果你更想先验证模型在辅导场景下的对话质量可以直接用模型对话入口试几轮https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite。如果打算把这个 Agent 长期跑在编码或自动化任务里比如批量生成练习题解析、自动整理学情报告可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding_planutm_campaignrewrite。接入细节和参数说明在文档里https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite。最后留一个我踩过的坑别急着把max_turns调大。教育对话轮次一多上下文膨胀会拖慢响应学生等三秒没反馈就跑了。先把单轮质量做扎实再考虑长会话。
返回列表