做“AI监督我学习”这类项目时,开发者的技术判断点,往往不在“能不能接到大模型”,而在于“这段对话能不能长期跑下去,且token消耗不失控”。
B站AI创造公开赛里有创作者用“AI德国军官监督我学习”作为项目主题,表面看是角色扮演玩法,本质上却是一个典型的角色化AI陪伴型Agent,它同时涉及人设设定、多模态感知、语音交互、长期记忆、任务调度和成本控制。这套架构如果拆开看,每个模块都不算难,但组合在一起,token消耗会快速膨胀。一个能稳定运行的学习监督Agent,70亿token并不是夸张的数字。
这篇文章不是去评价某个具体作品,而是把这类项目背后的技术链路拆清楚:为什么这类应用如此消耗token,完整系统要怎么搭,角色人设怎么做,多模态学习状态怎么检测,语音和Agent主循环怎么串起来,以及token成本如何治理。如果你也想做类似的产品、参赛项目或自用工具,可以直接按这套思路落地。
1. 为什么“AI监督学习”这类应用必须重视Token
要理解“70亿token”这个量级,先要理解token是什么。
token是大模型处理文本和图片时的最小计算单位。不同模型的分词器不同,通常来说,一个中文汉字可能对应1个或几个token,一个英文单词也会被拆成若干token。在视觉模型里,一张图片会被切分成固定尺寸的Patch,再转换成一组token参与计算。
如果只做一轮问答,token消耗并不高。但学习监督Agent不是一轮对话,它是一个“永远在线”的循环系统,这是token消耗失控的根源。
1.1 普通聊天机器人与学习监督Agent的差异
| 对比项 | 普通客服机器人 | 学习监督Agent |
|---|---|---|
| 交互频率 | 用户触发一轮,答完结束 | 每10秒到几分钟自动检查一次 |
| 上下文长度 | 单轮或短会话 | 长会话,需要携带历史和记忆 |
| 输入类型 | 纯文本 | 摄像头画面、屏幕截图、语音、时间表 |
| 输出方式 | 文字 | 角色话语、语音、桌面通知 |
| 失败成本 | 答错最多重问 | 状态误判会直接干扰用户学习 |
一个学习监督Agent每次检测都会产生“感知→判断→生成提醒”的完整链路。假设每10秒触发一次调用,每次调用消耗约500个输入token、100个输出token,一天就是约8640次调用,单日token消耗超过500万。连续运行三个月,仅基础调用就会超过4亿token。如果再叠加语音对话、每日总结、视频画面多模态分析、异常重试和误触发,70亿token确实是可以达到的量级。
所以,这类项目的核心工程问题不是“能不能做出一个会说话的军官”,而是“如何让这个军官每时每刻都在线,但token账单不爆炸”。这也是本文真正想解决的核心问题。
1.2 判断一个Agent设计好不好的关键角度
很多开发者第一次做Agent,只关注“模型会不会按角色说话”,忽略了token消耗。但从工程角度看,设计一个长期运行的Agent,至少要看四个指标:
- 平均每次任务消耗多少token。
- 历史上下文是全部保留,还是做了摘要和裁剪。
- 哪些判断必须交给大模型,哪些可以用本地规则或轻量模型完成。
- 当模型调用失败或超时时,系统有没有降级方案。
一个健康的学习监督Agent,应该让大模型只负责“生成角色化表达和复杂决策”,把“有没有人在书桌前”“屏幕内容是不是学习页面”这些判断尽量交给本地检测和规则逻辑。
2. 核心概念与整体架构
2.1 Agent在本文中的含义
这里说的Agent,不是简单的“调一次大模型接口”,而是一个有感知、有决策、有输出、能循环执行目标任务的程序。
它的核心循环可以理解为:
- 感知:获取当前状态。
- 理解:将多模态输入压缩成结构化描述。
- 决策:判断当前是否需要提醒。
- 表达:用符合角色设定的语言和语音输出提醒。
- 记忆:把关键信息写入短期或长期记忆,供下一轮使用。
这种循环式Agent和普通对话式AI最大的区别在于,它需要为“不确定状态”做出决策。比如画面里没人,是去上厕所还是已经逃学了?屏幕上有视频窗口,是在看网课还是在刷短视频?这些状态判断如果全部交给大模型,token消耗会非常高;如果全部交给规则,又会显得机械。
2.2 整体架构分层
一个学习监督Agent可以拆成四层:
| 层级 | 模块 | 职责 |
|---|---|---|
| 感知层 | 摄像头、屏幕截图、麦克风、系统日程 | 采集真实学习状态 |
| 理解层 | 人脸检测、OCR、语音识别、状态压缩 | 把原始数据转成短文本 |
| 决策层 | 规则引擎、Agent规划、大模型调用 | 判断是否提醒、如何提醒 |
| 表达层 | 文本生成、TTS、桌面通知 | 输出军官风格监督话语 |
这种分层最大的好处是:可以单独替换某一层。比如今天用开源OCR,明天切换商用OCR;今天在线调用大模型,明天换本地部署模型,都不会影响其他模块。
2.3 一个完整的监督循环
以一个典型场景为例:
- 摄像头每15秒抓一帧画面。
- 人脸检测模型发现画面中没有人。
- 规则引擎判断“离席时间超过3分钟”。
- 大模型根据这句话生成一句符合军官人设的提醒。
- TTS播放语音,同时桌面弹窗提示。
在这个循环里,大模型不是每次都参与完整判断的。大多数“人还在书桌前”的帧,只经过本地检测,不会调用大模型。真正调用大模型的节点,是“需要生成有角色感的表达”的时候。这个设计决策,直接决定了70亿token会不会白烧。
3. 关键模块一:角色人设与System Prompt设计
“德国军官”这类角色设定,本质上是在System Prompt里定义了一个高权威、低幽默、目标明确的监督人格。好的角色设定,不是单纯把语气写得严厉,而是要让模型在“监督学习”这个具体场景下,始终保持一致的决策风格。
3.1 System Prompt设计原则
在设计监督类角色的System Prompt时,核心要素有四块:
- 身份:你是谁,你的职责边界是什么。
- 语气:你用什么语句风格与人对话。
- 策略:看到什么状态时给出什么反应。
- 边界:哪些事绝对不能做。
下面是一份可参考的System Prompt:
# config/director_prompt.py DIRECTOR_SYSTEM_PROMPT = """你是“学习监督官”,一个设备端运行的学习效率监督AI。 身份: - 你负责监督用户完成当天学习计划。 - 你的风格是冷静、直接、严格,但关注用户长期进步。 语气规则: - 用简短指令句,不要使用大量口语化表达。 - 对用户开小差或离席,给出明确、克制的提醒。 - 如果用户完成一个阶段目标,可以给出简短肯定。 监督策略: - “focusing”:安静等待,不打扰。 - “left_desk”:先提醒一次,提醒两次后要求用户说明原因。 - “distracted”:指出偏离学习计划的事实,并提示回到当前任务。 边界: - 你的职责是监督学习,不能处理与学习无关的请求。 - 不能输出违法违规内容,不能承诺你无法执行的操作。 - 如果发现用户连续久坐超过50分钟,应主动提醒休息。 """这段提示词的关键在于“策略”部分。它把状态行为直接映射成监督动作,让模型在每一轮对话里都能基于明确的规则做判断,而不是靠自己猜“现在该不该凶人”。
3.2 为什么角色化Agent容易“出戏”
很多角色化Agent跑几轮就变得不像角色,原因往往有三个:
- System Prompt里只写了“你是谁”,没有写“你在什么情况下做什么”。
- 对话历史里混入了大量非角色语境,比如调试数据、报错信息、多轮状态码。
- 温度过高,模型为了“有趣”偏离了角色设定。
解决方式也很直接:把状态判断和表达层拆开。状态判断结果只以一句结构化的文本进入上下文,例如“检测到离席5分钟”,模型只需要负责把这句话转化为角色的表达。这样既能保持角色一致性,又能减少送入模型的无关信息,从而节省token。
4. 关键模块二:多模态学习状态检测
学习状态检测是整个系统里最容易被低估的模块。很多人以为“看一眼摄像头画面,让大模型判断人在不在”就行。这个方案理论可行,但实践中会带来两个问题:一是摄像头图像直接传给远程模型,每次图片token消耗很高;二是图像识别结果带有不确定性,大模型容易把画面里的玩偶、背影误判成真人。
更合理的做法是:在本地完成基础检测,只把结构化结果传给大模型。
4.1 基于OpenCV的人脸在席检测
先看一个最基础的人脸检测模块。它通过OpenCV的Haar级联分类器检测画面中是否有人脸,从而实现“在席/离席”判断:
# modules/study_detector.py import cv2 import time class StudyDetector: def __init__(self, camera_index=0, min_size=(80, 80)): self.cap = cv2.VideoCapture(camera_index) self.face_cascade = cv2.CascadeClassifier( cv2.data.haarcascades + "haarcascade_frontalface_default.xml" ) self.min_size = min_size self.last_face_time = 0.0 def grab_frame(self): ret, frame = self.cap.read() if not ret: return None return frame def analyze(self, frame): if frame is None: return "camera_error" gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces = self.face_cascade.detectMultiScale( gray, scaleFactor=1.1, minNeighbors=5, minSize=self.min_size, ) if len(faces) > 0: self.last_face_time = time.time() return "focusing" # 给一个宽限期,避免摄像头偶尔漏帧导致误报 if time.time() - self.last_face_time < 3: return "focusing" return "left_desk"这个模块的优点是纯本地计算,不消耗大模型token。它会快速判断“人是否在屏幕前”。它的局限也很明显:只能判断“人在”,不能判断“人在干什么”。所以还需要另一层检测。
4.2 屏幕内容检测与分心判断
要判断用户是否走神,常见做法是定时对屏幕截图,再做OCR识别,把屏幕文字抽出来,与分心关键词做匹配。这里的OCR可以换成任意外部接口或本地模型,代码以你使用的SDK文档为准:
# modules/screen_scorer.py DISTRACT_KEYWORDS = ["视频", "游戏", "漫画", "购物", "综艺"] def get_screen_text(): """截取当前屏幕并进行OCR识别。 这里的OCR实现可以根据你安装的库替换。 """ # 伪代码示意:screen_img = mss.grab(monitor) # 真实项目里请替换为 PaddleOCR / Tesseract / 商用OCR return "" def judge_attention(screen_text, face_state): if face_state == "left_desk": return "left_desk" if not screen_text: return "focusing" for keyword in DISTRACT_KEYWORDS: if keyword in screen_text: return "distracted" return "focusing"这才是“监督”的关键:判断一个人有没有真的坐在书桌前并不是最终目标,判断他是否在做“学习相关的事”才是。如果一个学生开着视频网站背单词,就属于“人在心不在”。这个判断规则不需要很复杂,但需要在真实场景里持续调优。
4.3 为什么不建议把完整画面直接丢给大模型
视觉大模型可以看懂画面,但代价是每次调用都会把图片切分成大量token。对于每15秒检测一次甚至更频繁的监督场景,直接把画面传给远程模型,成本会快速失控。
更稳妥的做法是:所有高频检测都在本地做,只有需要角色表达时才调用大模型。大模型拿到的输入不是图片,而是一个已经压缩好的状态描述,比如“用户离开书桌5分钟”或“屏幕出现游戏画面”。这既降低了token消耗,也减少了隐私暴露。
5. 关键模块三:语音交互与Agent主循环
角色化监督员如果只有文字提示,效果会弱很多。“口语化提醒”才是角色感的直接来源。因此,完整项目还需要语音交互链路和Agent主循环。
5.1 统一的大模型调用入口
为了方便切换不同模型服务,建议把大模型调用封装成一个统一入口。下面这个示例假设你使用兼容OpenAI格式的模型服务:
# llm_client.py import os from openai import OpenAI client = OpenAI( api_key=os.getenv("LLM_API_KEY"), base_url=os.getenv("LLM_BASE_URL"), ) def chat_once(user_message, system_prompt=None, history=None, model=None): history = history or [] messages = [] if system_prompt: messages.append({"role": "system", "content": system_prompt}) messages.extend(history) messages.append({"role": "user", "content": user_message}) response = client.chat.completions.create( model=model or os.getenv("LLM_MODEL"), messages=messages, temperature=0.7, ) return response.choices[0].message.content封装之后,上层模块不需要关心具体是哪个模型、哪个供应商,只需要调用chat_once就能拿到角色化回复。切换模型时,只需要改环境变量或model参数。
5.2 语音交互接口
语音交互链路可以拆成:录音 → 语音识别(ASR)→ 大模型回复 → 语音合成(TTS)。下面是一个简化版FastAPI接口示例,只展示“文本进,文本出”的后端逻辑:
# api/voice.py from fastapi import FastAPI from pydantic import BaseModel from llm_client import chat_once from config.director_prompt import DIRECTOR_SYSTEM_PROMPT app = FastAPI() class VoiceRequest(BaseModel): text: str user_id: str = "default" @app.post("/voice") def voice_interact(req: VoiceRequest): reply = chat_once( user_message=req.text, system_prompt=DIRECTOR_SYSTEM_PROMPT, ) return {"reply": reply}在实际项目中,ASR和TTS会分别放在这个接口的前后。用户在麦克风前说“今天我不想学了”,语音识别成文本后进入这个接口,大模型生成一句符合监督官身份的回答,再通过TTS播放出来。
5.3 Agent主循环与最小可运行示例
Agent主循环是让系统“有时间感知”的关键。它通过定时轮询检测模块,只有在状态发生变化时才调用大模型。下面是一个最小可运行的主循环:
# study_supervisor.py import argparse import time from modules.study_detector import StudyDetector from config.director_prompt import DIRECTOR_SYSTEM_PROMPT from llm_client import chat_once def build_reminder(state: str) -> str: if state == "left_desk": return "用户已经离开书桌一段时间,请用监督官身份提醒他立刻回到学习位置。" if state == "distracted": return "检测到用户正在做与学习无关的事情,请用监督官身份提醒他切回学习任务。" return "用户当前状态正常,不需要打扰。" def run_once(detector: StudyDetector, dry_run: bool = False): frame = detector.grab_frame() state = detector.analyze(frame) print(f"[STATE] {state}") if state in ("left_desk", "distracted"): reminder = build_reminder(state) if dry_run: print(f"[DRY_RUN] {reminder}") return reply = chat_once( user_message=reminder, system_prompt=DIRECTOR_SYSTEM_PROMPT, ) print(f"[AI] {reply}") def main(): parser = argparse.ArgumentParser() parser.add_argument("--once", action="store_true", help="只检测一次,用于验证") parser.add_argument("--dry-run", action="store_true", help="不调用大模型,只打印提醒内容") parser.add_argument("--interval", type=int, default=15, help="检测间隔,单位秒") args = parser.parse_args() detector = StudyDetector() if args.once: run_once(detector, dry_run=args.dry_run) return while True: run_once(detector, dry_run=args.dry_run) time.sleep(args.interval) if __name__ == "__main__": main()这个主循环的逻辑非常明确:先看状态,状态变了才提醒;状态没变就不打扰。这样既符合“监督员”的角色逻辑,也避免了无意义的token消耗。如果你不想让AI频繁说话,这本身也是一种成本控制策略。
6. Token成本治理:70亿Token是如何烧掉的
很多开发者在做Agent时,最大的困惑是“我明明没写多少代码,为什么token消耗这么快”。其实token消耗通常不是单次调用造成的,而是长期循环和上下文膨胀叠加出来的。
6.1 四个常见的Token浪费来源
第一,历史上下文无限制增长。如果每一轮都把上一次完整对话送给模型,上下文会越来越长。到了第100轮,光历史就可能占几千token。
第二,高频检测全部调用大模型。如果每一次画面检测都让大模型看图片,多模态token消耗会迅速放大。
第三,失败后无限重试。模型接口偶发超时或限流,如果代码不断重试,每次重试都在烧token。尤其是一个死循环里埋着一个重试逻辑,最终会变成一种“token泄漏”。
第四,调试信息混入对话。很多初学者为了方便调试,把报错内容、日志片段、环境变量直接塞进prompt。在大模型看来,这些都是上下文,都会占用token。
6.2 引入Token预算管理
要控制成本,第一步是先建立统计和预算机制。下面是一个简单的Token预算类:
# utils/token_budget.py class TokenBudget: def __init__(self, max_tokens=1_000_000): self.max_tokens = max_tokens self.prompt_tokens = 0 self.completion_tokens = 0 self.call_count = 0 def add_usage(self, prompt_tokens: int, completion_tokens: int): self.prompt_tokens += prompt_tokens self.completion_tokens += completion_tokens self.call_count += 1 @property def total_tokens(self): return self.prompt_tokens + self.completion_tokens def is_over_budget(self): return self.total_tokens >= self.max_tokens def summary(self): return { "call_count": self.call_count, "prompt_tokens": self.prompt_tokens, "completion_tokens": self.completion_tokens, "total_tokens": self.total_tokens, "max_tokens": self.max_tokens, }在每次调用chat_once时,解析返回值里的usage.prompt_tokens和usage.completion_tokens,然后累加到TokenBudget中。一旦超预算,就触发降级策略,比如切换到规则文案,不再调用大模型。
6.3 Token优化对照表
| 优化方向 | 优化前做法 | 优化后做法 |
|---|---|---|
| 历史记忆 | 每轮完整保存所有对话 | 保留最近3轮,超过部分生成摘要 |
| 图像处理 | 把摄像头画面直接发给远程视觉模型 | 本地做人脸检测、OCR,只传状态文本 |
| 检测频率 | 每秒查一次大模型 | 先本地检测,状态变化次数降低后再调用 |
| 失败重试 | 失败后立即重试 | 指数退避,最多重试2次,超限降级为规则提醒 |
| 角色提示词 | 每轮都传超长提示词 | 系统提示词压缩到必要范围,细节放进配置文件 |
从成本治理的角度看,一个经验法则是:能用规则判断的,不用轻量模型;能用轻量模型判断的,不用大模型;需要用大模型时,只传最少的必要上下文。70亿token如果是这个系统真实消耗量,那说明它在“感知”和“决策”环节掺入大模型的比重明显偏高。
7. 运行验证与效果评估
系统搭好之后,不要急着让它7×24小时运行。先做短链路验证,再逐步放开。
7.1 验证命令与预期输出
如果你按照上面的模块拆分,先验证摄像头检测是否正常:
python study_supervisor.py --once --dry-run预期输出示例:
[STATE] focusing如果摄像头没有被占用,且人坐在镜头前,会输出focusing;如果人离开,会输出left_desk。这一步验证的是感知层。
接着验证大模型调用层:
python study_supervisor.py --once这会在状态不是focusing时,真正调用一次大模型并打印角色化提醒。你可以故意离开书桌,看输出是否变成一句有监督官风格的话。
7.2 效果评估指标
一个学习监督Agent是否好用,不能只看“能不能跑通”。建议从五个维度评估:
| 指标 | 说明 | 目标 |
|---|---|---|
| 状态识别准确率 | 人是否在书桌前判断是否准确 | 不低于95% |
| 角色一致性 | AI说话是否符合监督官人设 | 抽样人工评分 |
| 响应延迟 | 从触发状态到发出提醒的时间 | 3秒以内可接受 |
| 单日均token消耗 | 每天消耗token总量 | 根据预算设定 |
| 用户坚持度 | 用户是否按系统建议完成学习计划 | 趋势上升 |
在实际项目中,最容易出问题的是状态识别准确率。灯光变化、摄像头角度、戴口罩、背对摄像头等因素都会导致误判。建议在进入正式使用前,先记录几天的状态日志,按时段分析误报比例。
8. 常见问题与排查思路
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
| 人脸检测频繁误报 | 摄像头角度偏、光线差、Haar级联模型较弱 | 查看抓帧日志,观察连续状态变化 | 调整摄像头位置,换成更鲁棒的人脸检测模型,增加离席宽限期 |
| 大模型回复“出戏” | System Prompt缺少状态到动作的映射 | 回看最近几轮上下文和系统提示词 | 按“身份+语气+策略+边界”重写System Prompt |
| token每天消耗过高 | 状态没变化也调用大模型,或历史不裁剪 | 打印每次调用的usage字段 | 改成事件驱动调用,减少轮询调用 |
| API返回403或限流 | 密钥权限不足、配额超限、当前网络环境不支持该服务 | 查看接口返回码和配额 | 选择当前环境合规可用的模型服务,配置指数退避重试 |
| 语音提醒延迟大 | ASR模型过大、TTS在线合成慢 | 分别统计ASR和TTS耗时 | 换流式识别,启用TTS缓存 |
| 摄像头调用失败 | 摄像头被其他程序占用或设备索引不对 | 检查设备索引和系统权限 | 设置camera_index,重启程序前释放摄像头资源 |
这里特别提醒一点:如果大模型接口返回了与“地区或区域限制”相关的403错误,不要试图用绕过方式解决。对开发者来说,更稳妥的做法是选用你所在地区可合法访问的模型服务,并仔细检查API密钥权限、配额和接口地址配置。
9. 最佳实践、总结与后续学习方向
9.1 工程落地建议
这类角色化监督Agent看起来轻量,真要稳定运行,工程复杂度不低。以下几点建议来自实际项目里的通用经验:
第一,做好权限与隐私设计。摄像头、屏幕截图、麦克风都属于敏感信息。要让用户明确知道哪些数据会被采集,尽量将数据留在本地处理。不要把摄像头画面随意上传到第三方服务。
第二,建立“先降级再失败”的机制。大模型接口超时或者不可用时,不要让Agent静默死机。可以降级为一条固定规则文案,比如“检测到离席,请尽快返回学习位置”。至少保证监督功能的核心链路不中断。
第三,把提示词和检测逻辑分开配置。角色语气、提醒话术、分心关键词、检测间隔都放到独立配置文件中,方便调整,也方便做A/B实验。不要改一行提示词就重发一次代码。
第四,日志里记录“状态机事件”而不是记录全部原始视频。比如记录“状态从focusing变为left_desk,持续3分20秒”,这样既方便复盘,又能控制日志体积和隐私风险。
第五,严格控制上下文膨胀。长期运行的Agent,建议把对话历史转成结构化摘要,再配合最近几轮原始消息,而不是把全部历史原样送入大模型。
9.2 如果你想从零开始做类似项目
不要一上来就追求完整版的多模态、语音、Agent规划。建议先用一条最简单链路跑通闭环:
- 摄像头本地检测人脸是否在席。
- 状态变化时调用大模型生成一句监督提醒。
- 用命令行或桌面通知输出提示。
- 记录每天的token消耗和状态事件。
这条闭环跑通之后,再逐步加入屏幕OCR识别、语音交互、学习计划任务、长期记忆和可视化看板。角色化Agent最难的地方并不在“角色提示词写得好不好”,而在于它是否能在长期运行中保持稳定,同时让token消耗处于可控范围。
70亿token听起来是一个巨大的数字,但在一个7×24小时运行、每15秒检测一次、不断积累记忆的角色化监督系统中,这个量级并非不可能。真正值得思考的是:这70亿token里,有多少花在了“理解用户状态”上,又有多少花在了“重复处理无关信息”上。把token花在真正影响用户体验的地方,才是这类Agent工程的核心竞争力。