消息组装与缓存设计:读源码理解LLM应用关键

消息怎么拼,可能比模型本身更影响效果。我用几周时间把项目里负责消息组装的 Hermes 模块源码啃了一遍,发现真正值钱的设计不在“把字符串塞进去”,而在角色边界、预算控制、缓存键这三件事上。这篇文章就是那轮走读的完整记录,适合正在做 LLM 应用、想优化 Prompt 组装或排查上下文异常的开发者。如果你以为提示词组装只是 f-string 多拼接几次,看完你会改变想法。

1. 组装和缓存为什么值得单独拆一章

1.1 消息组装不是字符串拼接,是协议转换

很多人第一次接触 Prompt 时觉得它简单:把用户输入放模板里就行。实际上到了源码层面,消息组装做的是协议转换——把外部请求的数据结构转成模型接口认识的内部规范消息流。这个转换过程涉及的不只是文本,还有角色标记、系统指令注入、多轮对话顺序还原,甚至要处理图片等多模态占位符。

Hermes 模块里有一个很典型的抽象叫 MessageTuple,它把每条消息定义成 (role, content, meta) 三元组。role 决定这条消息以谁的口吻出现,content 是实际载荷,meta 则记录这条消息的来源、时间戳、是否系统生成等信息。组装的核心逻辑就是围绕这个三元组展开的:从请求参数里提取 User 消息,把历史会话记录按时间排序还原成多轮结构,再把系统模板里的静态指令放到最前面。

如果你只是做字符串拼接,至少会遇到四个问题:角色顺序错乱、指令被用户输入覆盖、多轮对话裁掉不该裁的历史、模板变量丢失导致渲染失败。源码里加的那一层抽象,本质上是给所有“拼接操作”设定了约束条件,让错误在一开始就被类型系统挡掉,而不是在推理时才暴露。

所以理解组装,先别盯着填充字符串的哪一行代码,要看清楚设计者怎么定义“一条消息”。消息不是一个字符串,而是一个带角色的结构化对象。缓存也是基于这个对象来构建的,不是基于渲染后的纯文本。这一点在后文讲缓存键时会展开,它是整套设计的基石。

1.2 缓存解决的不是速度问题,是成本与稳定性问题

提示缓存常被误解为性能优化。从源码设计角度看,它更多是成本和稳定性的权衡。同一个系统提示加上重复的上下文片段,如果每次请求都重新渲染模板、重新截断、重新计算 token 预算,不仅浪费 CPU 时间,还会让同一段文本在多次请求中产生不同的截断结果,给线上问题排查带来干扰。

Hermes 的缓存模块把“渲染好的 prompt 结构”缓存下来,而不是只缓存文本。这意味着模板渲染、变量填充、预算裁剪这些步骤都可以跳过。对长上下文场景,比如一个包含 2 万 token 系统指令和知识库摘要的请求,缓存命中能减少 30% 到 50% 的预处理耗时。

但缓存也有代价:内存占用、失效时机、键冲突。如果键设计得不够细,会命中错误的 prompt;如果失效策略太激进,缓存命中率上不去;如果太宽松,又会把不该复用的旧模板内容发给模型。所以这一章的代码走读,缓存实现里的每个细节都值得逐行看。

2. 核心数据结构:消息从哪来,要变成什么

2.1 消息数组:角色、内容、元信息三段式

组装的第一步是造消息数组。源码里定义了标准化的消息结构,每条消息必须携带 role、content 和 meta。role 只能是 system、user、assistant 三种之一,这个枚举约束保证了消息流在模板渲染时不会混乱。

工程实现上,源代码没有直接用 Python 的 tuple 或 dict 表示消息,而是定义了 dataclass:

@dataclass(frozen=True, slots=True) class Message: role: str content: str meta: dict[str, Any] = field(default_factory=dict) def to_dict(self) -> dict[str, str]: base = {"role": self.role, "content": self.content} if self.meta.get("name"): base["name"] = self.meta["name"] return base

frozen 和 slots 两个参数是关键。frozen 防止组装过程中消息被意外修改,slots 减少对象内存占用。消息一旦生成就不可变,这对缓存安全尤其重要——缓存里存的对象如果可以被外部修改,任何一次意外写入都会污染所有后续请求。

meta 字段是很多人会忽略的细节。它存了消息来源标识、原始时间戳、是否由系统自动生成等信息。这些元数据不直接发给模型,但会在缓存键计算、调试追踪、会话整理时发挥重要作用。我在读源码时特意关注了 meta 的传递链路,发现组装和缓存模块都依赖它做决策,比如判断某条消息是否需要绕过缓存。

组装器还会对消息数组做顺序校验。如果检测到连续两条相同 role 的相邻消息,会尝试合并;如果第一条消息不是 system 或 user,会直接抛出异常。这一层防御代码看似多此一举,实际在接入 WebSocket 实时流和异步回调时非常有用,因为那些场景的消息顺序经常乱。

2.2 模板注册表:变量与角色的绑定逻辑

模板注册表是另一个容易被低估的组件。它不只是一个模板仓库,而是维护着“变量名到类型”“变量名到默认值”“模板内角色边界”的映射关系。

看一下核心实现思路:

class TemplateRegistry: def __init__(self, loader: TemplateLoader): self._loader = loader self._defs: dict[str, TemplateDef] = {} def register(self, name: str, source: str, variables: dict[str, type]): parsed = self._parse(source) if not self._validate_variables(parsed, variables): raise ValueError(f"template {name} has unmapped variables") self._defs[name] = TemplateDef( name=name, source=source, variables=variables, renderer=JinjaRenderer(source), )

注册阶段就会校验模板里的所有变量是否都有映射,而不是等到渲染时才报错。这样做的直接好处是建站脚本或配置发布流程可以在上线前发现模板问题,不用等用户触发出错误提示。

变量校验通过一组显式声明来完成。比如某模板声明了变量knowledge_base,类型是list[str],那么渲染时如果传入字符串,注册表会做类型转换或直接拒绝。这个设计避免了“模板变量类型混乱导致模型收到奇怪格式”的隐蔽问题。

模板与角色绑定是注册表更高级的功能。以系统提示模板为例,注册表不仅记录模板文本,还记录该模板产生的消息应该挂在哪个 role 上。常规模板挂在 system 下,少部分动态内容模板挂在 user 下。组装器在读取模板时会自动带上这个属性,省去在业务层手动指定角色的麻烦。

2.3 预算控制:裁剪不是删末尾,是减层级

组装后半段最复杂的逻辑是 token 预算控制。源码里没有简单地在超长时截断末尾,因为末尾往往是模型要回答的问题本身,截掉就失去意义了。预算模块把 token 占用划分为系统指令、历史消息、当前输入三个层级,按优先级逐级压缩。

优先级排序是这样的:当前输入优先级最高,通常不裁;系统指令次之,只裁其中的背景资料部分,不裁指令性语句;历史消息优先级最低,优先裁最久远且与当前问题弱相关的片段。为了实现这个逻辑,源码给每段文本打上了预算级别标签:

class BudgetSegment: def __init__(self, level: str, text: str, tokens: int): self.level = level # "system_core" | "system_bg" | "history" | "current" self.text = text self.tokens = tokens

预算模块先将组装好的消息列表展平成文本段,再为每段估算 token 数,接着按 level 审查总占用。如果超出上限,先降history级别段的权重,删除最久远片段;还不够,再压缩system_bg,但保留system_core和current不动。

读这段源码时我最感慨的是 token 估算的粒度。它不是对整个消息列表一次性估算,而是对每个 BudgetSegment 单独估算。这就让裁剪器能够做“分段丢弃”,而不是对整段文本做无差别截断。配合缓存模块使用时,同一个历史消息段即使从预算中被裁剪掉,原始渲染结果仍会保存在缓存中,一旦预算放宽还能快速恢复。

3. 消息组装核心代码走读

3.1 从请求到消息列表:三路汇合的组装流程

组装器的入口函数同时接收三类输入:请求中的新消息、会话上下文中的历史消息、模板注册表中解析出的系统消息。这三路信息汇合后,先做标准化,再做排序,最后送入渲染层。伪代码大致如下:

class MessageAssembler: def build_messages(self, request: ChatRequest) -> list[Message]: system_message = self._templates.build_system_message( request.template_name, request.template_vars ) history_messages = self._history.to_messages(request.session_id) new_message = Message( role="user", content=request.input_text, meta={"request_id": request.request_id}, ) ordered = self._apply_order( [system_message, *history_messages, new_message] ) return self._deduplicate(ordered)

_apply_order不简单只是拼接。它接收一个 messages 列表和一个 sort key,把系统消息固定在最前,历史消息按会话内时间戳升序排列,新消息放在最后。如果系统消息缺失(比如模板注册表没有找到对应模板),组装器不会自动补空消息,而是抛出缺模板异常,让上层快速感知配置问题。

_deduplicate会移除完全重复的消息,但不会移除内容相同但 meta 不同的消息。这个细节在调试时很重要:两条内容相同的历史消息,meta 里的时间戳不同,说明它们来自不同会话时刻,不能合并。

走读这段代码时,我还注意到组装器不做任何文本截断,任何截断都留给预算模块处理。这个单一职责原则保证组装器可以被单独测试:给定输入,断言输出消息列表是否有序、角色是否正确,完全不依赖 token 计算。

3.2 模板渲染:Jinja2 之上的安全层

模板渲染层是组装链路中最容易出安全问题的地方。原生 Jinja2 渲染允许任意表达式执行,如果把用户输入直接塞进模板,就等于把注入漏洞暴露给外部。Hermes 的替代方案是包一层安全渲染器,在调用 Jinja2 之前先做变量白名单过滤。

安全渲染器的实现思路如下:

class SafeRenderer: def __init__(self, template_text: str, allowed_vars: set[str]): self._env = Environment( autoescape=False, undefined=StrictUndefined, loader=BaseLoader(), ) self._template = self._env.from_string(template_text) self._allowed_vars = allowed_vars def render(self, variables: dict[str, Any]) -> str: cleaned = {k: v for k, v in variables.items() if k in self._allowed_vars} missing = self._allowed_vars - set(cleaned.keys()) if missing: raise TemplateMissingVars(missing) return self._template.render(**cleaned)

StrictUndefined 配置是第二次防御:模板中任何一个未定义变量都会触发异常,而不是渲染成空字符串。这样能避免“变量名拼错但模板照常输出”的静默失败,让所有问题在开发期暴露。

这里有一个我在走读时特别留意的点:autoescape 为什么关闭?因为消息内容里可能包含 HTML 标签,如果开启自动转义,模型收到的就是转义后的实体,语义被扭曲。安全边界靠变量白名单和严格未定义来保证,而不是靠转义。这个取舍值得做 LLM 应用的同行记下来。

3.3 组装后的最终形态:结构化数据而非单字符串

组装完成后产生的对象不是单一字符串,而是一个 PromptComposite 结构,内部包含消息数组、token 预估、段级别信息。模型调用层拿到的是这个结构,而不是直接拼好的文本。

看这个结构定义:

@dataclass class PromptComposite: messages: list[Message] segments: list[BudgetSegment] total_tokens: int budget_level: str def to_payload(self) -> list[dict]: return [m.to_dict() for m in self.messages]

to_payload 是最终交给模型请求体的序列化方法。它会做一次最后的角色校验,确保每个 role 都在白名单里。这个校验不是多余防御,我在调试时遇到过 role 被业务层写成 “robot” 导致模型直接拒绝请求的情况,这层校验能在发送前 100 毫秒内拦住问题。

组装后的消息列表也可以被缓存序列化。序列化格式用的是 JSON + 版本号,版本号当前是 1。后续如果消息结构加了新字段,版本号一并升级,避免旧缓存被新代码读取时出现 KeyError。

4. 提示缓存核心代码走读

4.1 缓存键的生成:字段顺序也能决定错误率

缓存键设计是整个缓存模块的灵魂。组装结果如果只依赖模板名做键,任何上下文变化都会命中旧缓存,造成消息串号。Hermes 的缓存键采用分层哈希,把模型名、模板名、上下文摘要、用户标识等字段按固定顺序拼接,再做一次 SHA-256。

源码里键生成器长这样:

def build_cache_key( model: str, template_name: str, context: dict[str, Any], user_tag: str, ) -> str: digest = hashlib.sha256() for part in (model, template_name, user_tag): digest.update(part.encode("utf-8")) digest.update(b"\x1f") serialized_ctx = json.dumps(context, sort_keys=True, ensure_ascii=False) digest.update(serialized_ctx.encode("utf-8")) return digest.hexdigest()

字段顺序不是随意定的。把 model 放在最前面,是为了在多模型共建场景下天然隔离不同模型的缓存。template_name 放在第二位,保证不同模板之间不会误命中。context 用 sort_keys 序列化,确保同一上下文无论传入顺序如何,生成的键都一致。

这里有一个容易踩坑的地方:如果 context 过大,整个对象参与哈希会导致键生成 I/O 时间变长,甚至超过缓存命中的收益。实际工程中会给 context 做摘要后再入键,比如只取长度超过 1000 的字段的前 50 个字符加哈希。源码里设计了摘要阈值,我后续会在调试实录里再展开。

user_tag 的存在是为了隔离不同用户的数据。多租户场景下如果缓存键里没有用户维度,A 用户的消息上下文就可能被 B 用户命中,这是明确的数据隔离事故。即使在同一用户下,会话 ID 也应参与哈希,不过 Hermes 将 session_id 放进了 context 字段,由调用方决定是否传入。

4.2 缓存命中与淘汰:TTL 叠加 LRU 的双层策略

缓存存储核心是一个带过期时间的 LRU 字典。每条缓存记录有三个字段:prompt 本体、expires_at 时间戳、last_access 访问计数器。读取时先查字典,再校验过期时间,过期则直接删除。插入时如果容量已满,按 last_access 升序淘汰最久未访问的记录。

这段实现有四个细节值得抄作业。第一,last_access 不是墙钟时间,而是自增计数器。计数器避免了系统时钟回拨导致 LRU 排序紊乱。第二,过期检查是惰性的,只有访问到该键时才判断是否过期,不依赖后台定时任务。这样既简单又省资源。第三,淘汰时如果有多个同年份记录,字典遍历顺序基本符合插入先后,虽然不是严格 FIFO,但工程上可接受。第四,淘汰一次只删除一个键,容量充足性由插入方的预检查保证。

缓存记录里还保存了预算版本号。每次模板注册表更新或预算参数调整,版本号会递增,所有旧记录即使在有效期内也会被视为失效。这个机制解决了“模板改了但缓存还是旧结构”的隐蔽问题。

4.3 缓存序列化与反序列化的一致性

缓存如果只存在于进程内存,重启后全部丢失。生产环境下 Hermes 模块将缓存扩展到 Redis,序列化协议就需要与内存模式保持一致。序列化时保存版本号、消息数组、segment 级别信息。

反序列化时有一个容易被忽略的校验点:消息数组里的 role 是否合法。如果新版本代码移除了某个角色,旧序列化数据里还留着,反序列化后直接发给模型就有风险。源码在反序列化时做严格校验,任何非法角色都会触发丢弃该缓存,而不是尝试修复。

另一个一致性细节是 token 计数。序列化时保存的 total_tokens 是基于当时分词器计算的值,如果代码部署了新分词器,旧值可能不准确。所以缓存记录里也保存了 tokenizer 版本号,读取时不匹配就视为无效,强制重新组装。

5. 调试实录与避坑心得

5.1 组合键冲突:模板名当成键名用了

接入初期我遇到过一个问题:两个会话的缓存键区分不开,总是命中同一条 prompt。查了半天才发现是缓存键生成代码里把模板名当成了键名使用,模板名相同但上下文不同的请求被归到了同一个键下。换成完整哈希键后问题消失。

这段经历让我体会到设计缓存键时的“最小充分”原则:键不能太粗,也不能太细。太粗会把不同上下文混在一起;太细又导致每次请求缓存都失效。一个合理的经验是,把所有会影响渲染结果的字段纳入键计算,但不包括时间戳之类每次请求必然变化的字段。

调试时可以用一个临时工具把所有缓存键打印出来,人工检查有哪些键对应同一模板但上下文摘要不同。这个动作在实际项目中比任何单元测试都更快发现问题。

5.2 截断位置不对:模型迷失在上下文中

另一个印象深刻的问题是预算裁剪模块把历史消息的截断位置放在了消息中间,导致模型收到了半个消息角色。后来我在预算模块中加了段边界校验,要求每个裁剪点都必须落在消息边界上。

具体做法是:切割段列表时,如果某段是从一条消息的 content 中间切开的,就把整条消息移入丢弃区,而不是保留半条。牺牲一点 token 空间,换取消息结构完整性。实际效果是模型回答的连贯性明显提升,上下文粘连导致的格式幻觉减少很多。

值得补充的是,裁剪策略本身也不应该一刀切。历史消息里的工具调用结果与普通聊天记录不同,工具结果被截断后模型会产生“工具没调完”的误解。所以我对工具消息段单独设置了较高的保留优先级,这一改动来自一次线上 badcase 的复盘。

5.3 缓存与预算参数不同步:旧缓存带来新错误

改过一次预算参数之后,我发现部分请求仍然走旧参数逻辑。原因是缓存命中发生后组装器直接用缓存结果,不再重新执行预算模块。旧缓存里保存的 total_tokens 和段级别信息基于旧参数,自然与当前配置不符。

这个问题的修复思路是把预算参数版本写进缓存键。预算版本变化后,旧键直接失效,新请求按新参数重新组装。虽然这种方案会让一次参数变更后缓存命中率短暂下降,但换回来的是结果正确性,这笔交易完全划算。

类似地,模板文件的任何变更都必须触发版本号更新。我在源码里加了一个模板文件内容的 MD5 校验,模板文件变了,生成出来的系统消息体积也变,缓存自然失效。从效果上看,这套联动机制解决了很多“改完模板但没生效”的初级问题。

5.4 观察组装与缓存的一手数据:调试技巧

分享一个判断组装结果是否正确的通用方法:在组装入口处打印结构化摘要,摘要包含消息总数、各角色占比、token 预算分配。不要打印完整消息内容,内容太长且容易刷屏,结构化摘要一眼就能看出问题。

我在本地开发环境封装了一个 debug_flag 环境变量,开启后组装器会往日志里输出 key 的前 12 位、模板名、消息数和总 token 数。排查问题时重点看三样东西:system 消息是否只有一条、历史消息是否按时间排序、总 token 是否接近预算上限但未超出。这三个信号的异常通常能直接定位到组装还是缓存环节。

还有一个值得提的细节是加日志别忘打耗时。组装全链路正常应该在 20 到 40 毫秒内完成,超过 100 毫秒就要怀疑是模板渲染慢还是哈希计算耗时。缓存命中时组装链路可缩短到 5 毫秒以下,如果观察不到这个差异,说明缓存可能没有在真正生效,要回头检查键生成逻辑。

个人体会:组装与缓存是一体的,别拆开看

读完 Hermes 这个模块,我最深的体会是组装和缓存不能割裂理解。缓存的键设计依赖组装阶段的变量体系,组装的裁剪逻辑又要参考缓存中保存的段信息。把这两块代码当作一个整体来维护,比单独优化任何一边都更有效。

如果要给后来者一个建议,那就是先看数据结构,再看流程,最后才看渲染细节。消息三元组、BudgetSegment、缓存键这三组结构定义清楚了,整个链路的行为逻辑就已经明了一大半。

我自己实际动手做的最有价值的一件事,是写了一个对照测试:固定输入内容,分别走“全新组装”“缓存命中”“裁剪后组装”三条路径,然后把三种结果的 token 数和消息结构做 diff。这个测试在后续每次改动时都会跑一遍,省掉了大量人工调试时间。你也可以按这个思路搭一个最小验证脚本,把组装器和缓存模块的输入输出抓下来存成 JSON,每次改完代码直接对比差异,比任何文档都直观。

相关新闻

AI助手跨会话记忆的双闸门设计:用有界与冻结解决遗忘污染失真

AI助手跨会话记忆的双闸门设计:用有界与冻结解决遗忘污染失真

1. 跨会话记忆的"三座大山":遗忘、污染与失真 我做 AI 助手有一段时间了,最常被用户吐槽的一句话是:你怎么什么都不记得?用户上周刚说过的偏好,这周换一个会话进来,又要重新交代一遍。放在纯聊天…

2026/10/20 16:11:34 阅读更多 →
Hive UDF实战指南:从简单脱敏到GenericUDF与UDTF的完整开发流程

Hive UDF实战指南:从简单脱敏到GenericUDF与UDTF的完整开发流程

数仓开发这行干久了,谁还没在 Hive 里遇到过内置函数搞不定的需求。比如日志里塞了一整段自定义协议串,比如业务要按身份证号做脱敏,又比如一个字段里存了七八个标签等着按分号拆成多行。Hive 3.1.2 自带几百个内置函数,覆盖了日常…

2026/10/20 16:11:34 阅读更多 →
数据结构面试高频题全解析:从数组到堆的考点与实战

数据结构面试高频题全解析:从数组到堆的考点与实战

1. 整体策略:数据结构面试到底在考察什么 准备数据结构面试,很多人的第一反应是去刷题网站猛刷几百道题,但刷完回来发现面试时照样发懵。我见过不少候选人在某平台上刷了三百多道题,结果面试官问一个“为什么这里用队列而不用栈”…

2026/10/20 16:11:34 阅读更多 →

最新新闻

大模型入门必读:从深度学习原理到工程实战的系统书单

大模型入门必读:从深度学习原理到工程实战的系统书单

最近这半年,总有人在后台问我:“我想入门大模型,该看什么书?”老实说,每次被问到这个问题,我都挺为难的。现在网上关于大模型的书单五花八门,有讲算法的、有讲应用的、有讲产业趋势的&#xff0…

2026/10/20 17:54:48 阅读更多 →
拆解DeepSeek Agent框架:插件化架构的设计与实战踩坑记录

拆解DeepSeek Agent框架:插件化架构的设计与实战踩坑记录

我一直有个习惯:拿到一个开源项目,先不看 README 也不看官方博客,直接把仓库 clone 到本地,先翻目录结构、再读接口定义,最后才回去对照文档验证自己的猜想。这次拆 DeepSeek 开源的 Agent 框架,起初只是好…

2026/10/20 17:54:48 阅读更多 →
AI助理代登录的权限设计:Poppy草案的四层授权关卡与落地实践

AI助理代登录的权限设计:Poppy草案的四层授权关卡与落地实践

你有没有想过,有一天你的AI助理会替你登录各种账号?不是那种简单的“帮我查一下天气”,而是真的拿着你的身份,去另一个平台做事情——比如帮你把昨天的报销单提交到财务系统,帮你把一个长邮件回复整理好并发送出去&…

2026/10/20 17:54:48 阅读更多 →
腾讯云AI语音识别:自动生成会议纪要的实践与调优

腾讯云AI语音识别:自动生成会议纪要的实践与调优

我们每次开会一小时,纪要却要整理半小时,这句话我从不止一个团队嘴里听到过。会议内容总要有人记,记完还要提炼待办、确认负责人,稍微复杂一点的讨论,光靠人工回忆很容易漏掉关键信息。所以这两年我一直建议有条件的团…

2026/10/20 17:54:48 阅读更多 →
Kubernetes资源清单实战:从YAML编写到Pod、Service与ConfigMap

Kubernetes资源清单实战:从YAML编写到Pod、Service与ConfigMap

1. 资源清单是什么,为什么是Kubernetes的第一道门槛很多刚接触Kubernetes的开发者,第一个卡住的地方往往不是Pod、Deployment这些概念本身,而是搞不懂"资源清单"到底是个什么东西、为什么要写这么一长串YAML。我最早学K8s的时候也踩…

2026/10/20 17:54:48 阅读更多 →
Qt课程进度导航栏G03:从数据模型到自绘渲染的完整实践

Qt课程进度导航栏G03:从数据模型到自绘渲染的完整实践

做教育类桌面客户端的时候,课程导航一直是个绕不开的活儿。用户要在一个窗口里看到章、节、课时的层级关系,要清楚自己学到哪了,还得能一键跳回上次退出的位置。市面上的现成组件大多只解决“展示目录”这一层,进度状态、学习记忆…

2026/10/20 17:53:47 阅读更多 →

日新闻

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

CLRC663芯片Pin-to-Pin替代实战:从评估到量产全记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
ESP32舵机控制指南:原理、多路驱动与避坑实战

ESP32舵机控制指南:原理、多路驱动与避坑实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →
位移法实现一阶低通滤波:单片机ADC采样降噪实战

位移法实现一阶低通滤波:单片机ADC采样降噪实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 1:01:30 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型(GMM)的语音识别系统,很多人第一反应就是一堆数学公式,觉得门槛很高。但如果你真的在MATLAB里把它落地跑通,会发现核心其实就是三件事:把声音变成特征向量,用GMM给每个类别…

2026/10/20 10:02:13 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人,但落到具体操作上,无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程:对图像做反白、改调色板、彩色转灰度,最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词,你会发现一个很有意思的现象:每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型,有人指的是运动控制系统,还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/20 2:50:50 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 10:02:08 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:17 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/20 9:50:45 阅读更多 →