山东大学项目实训-创新实训-个人博客(四)
山东大学项目实训-创新实训-个人博客(四)
系统架构概览
本项目构建了一套基于 LLM 的情绪驱动机器人交互系统,整体采用事件驱动架构,分为三层:Web 端通过 HTTP POST 将自然语言文本发送至 Agent 服务;Agent 端(OpenClaw)调用大语言模型完成 NLU(自然语言理解)并将结果序列化为动作指令,推入内存队列;机器人端(DuoS)以 2 秒为间隔轮询 Webhook Server 的/poll/{client_id}端点,取到指令后执行对应动作,并通过/ack/{client_id}回调确认。我负责 Agent 端的设计与实现,核心职责是构建从自然语言到结构化动作序列的推理链路。
原有方案的瓶颈
初版方案采用的是情绪分类 + 规则映射的 pipeline:将输入文本通过 LLM 分类为预定义情绪标签(happy、sad、angry 等共 8 类),再通过硬编码的查找表(lookup table)映射到固定动作序列。
这套方案存在两个根本性缺陷:
语义压缩损失过大。情绪标签本质上是一种有损编码,将连续的语义空间强行离散化为 8 个类别。“我很开心"与"老师当众表扬了我,我激动地冲出教室告诉朋友”,在分类器眼中都是happy,输出的动作序列完全相同,丢失了所有叙事细节。
长文本处理能力缺失。规则映射方案的输入是单一情绪标签,无法处理包含情节转折的长文本。系统对整段故事只能输出一个静态动作序列,无法体现叙事的时序结构。
新方案:基于 LLM 的两阶段故事解析
新方案放弃情绪分类中间层,改为让 LLM 直接承担叙事理解与动作编排的职责,形成端到端的 story-to-action 推理链路。
第一阶段:语义分段(Semantic Segmentation)
第一次 LLM 调用执行叙事分段任务。系统提示词(system prompt)将模型定义为"故事情节分析师",要求其按情节节点(plot node)对输入文本进行分段,而非按句法边界(句号/逗号)切割。输出约束为纯 JSON 字符串数组,强制去除解释性文本,降低后续解析的不确定性。
分段失败时触发降级策略(fallback):将原始输入作为单一片段继续处理,保证 pipeline 不中断。
第二阶段:动作序列生成(Action Sequence Generation)
对每个情节片段,执行第二次 LLM 调用,模型角色定义为"机器人动作编导"。可用动作集合共 11 个原子动作(atomic action),编号 0-10,覆盖位移(前进/后退)、姿态(下蹲/站立)、交互(挥手/致谢)、高强度表达(舞蹈/飞翔)等类别。
prompt 中明确规定输出格式为纯 JSON 整数数组,并注入以下编排约束:
- 动作排列须符合物理直觉与叙事逻辑(locomotion 动作先于 interaction 动作)
- 高强度动作(编号 6/7)须置于段落高潮位置,禁止出现在序列首位
- 末位动作必须为 0(待机),作为状态复位信号
last_action 跨段上下文传递
多段生成的核心挑战是段间连续性(inter-segment continuity)。若每段独立生成,相邻片段的边界处极易出现语义跳跃(semantic jump),导致动作序列在视觉上产生卡顿感。
解决方案是在每次调用generate_actions()时,将上一段最后一个非零动作编号作为last_action参数注入 prompt,约束当前段首个动作与其语义距离不能过大。每段执行完毕后更新last_action状态,形成滑动窗口式的上下文链。
动作时长约束与差异化 Prompt 策略
实测发现动作6(舞蹈)执行时长为 60 秒,若在叙事中途触发,会严重破坏故事节奏,导致观众注意力完全转移。
针对这一问题,设计了单段/多段差异化 prompt 注入策略:
is_multi_segment=False(单段输入):不注入铺垫约束,允许[6, 0]直接输出is_multi_segment=True(多段故事):在 user prompt 中追加强约束——动作6前必须存在至少一个铺垫动作,禁止[6, 0]直接结尾
此策略在不修改动作编号定义的前提下,通过 prompt engineering 实现了场景感知的动作编排控制。
序列拼接与 normalize 兜底
多段序列通过merge_sequences()合并:中间段去除结尾的 0,最后一段保留。合并后统一经过normalize_action_sequence()处理:过滤非整数值、过滤超出 0-10 范围的编号、强制末位补 0。这一兜底机制确保无论上游 LLM 输出何种异常,下游机器人端始终接收到合法指令。
效果对比
输入:小明走进教室,老师表扬了他,他激动地跑去告诉朋友
| 方案 | 动作序列 | 说明 |
|---|---|---|
| 情绪分类映射 | [1, 6, 0] | 整体 happy,查表输出固定序列 |
| 故事解析(新) | [9, 3, 9, 1, 0] | 前进→致谢→前进→挥手→站立,还原叙事节奏 |
工程实践
Provider 抽象层:将 LLM 调用封装为可替换 provider 接口,支持dashscope(通义千问)与mock(本地测试)两种实现,通过环境变量EMOTION_PROVIDER切换,输出 schema 完全一致,为后续模型迁移提供零侵入替换能力。
配置安全化:去除所有硬编码 API Key 与绝对路径,统一通过.env文件注入,并实现了无第三方依赖的轻量 dotenv 解析器,兼容 Windows/Linux 跨平台部署。
接口向后兼容:所有重构发生在 skill 内部逻辑层,对外暴露的 Webhook 接口(端点路径、请求/响应 JSON schema、动作编号定义)完全冻结,机器人端代码零改动。segments调试字段仅在analyze_emotion.py内部输出,server.py对外响应时剥离,避免接口污染。
当前进度与下一步
Agent 端故事解析链路已完成本地开发、真实 API(通义千问)端到端验证,并通过 SSH 部署至生产服务器。系统当前可处理包含多情节转折的中文叙事文本,输出具备叙事弧线(narrative arc)的动作序列。
下一步计划联调机器人端,在真实硬件执行环境中验证动作编排效果,并根据实际帧间表现进一步调优 prompt 约束与序列长度参数。
