news 2026/10/6 9:37:10

ClickHouse读取缓存详解:从Page Cache到Query Cache

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ClickHouse读取缓存详解:从Page Cache到Query Cache

同一个 SQL,第一次跑要 3 秒,第二次只要 0.3 秒。这种体验在 ClickHouse 里太常见了,但很多人不知道,这个差距背后的主角是一整套读取缓存体系,而不只是某一条配置。这篇先聊读路径上的缓存,下一篇再聊写入侧的缓存和后台任务对缓存的破坏。如果你正在用 ClickHouse 做分析报表、实时同步链路,或者正在折腾部署调优,建议把这篇看完。

我见过不少人遇到"查询时快时慢"就直接去调max_threads、改内存参数,折腾半天没效果。其实问题大概率出在缓存层:是文件系统缓存被挤掉了,还是 mark 缓存没命中,还是查询结果缓存压根没开。搞清楚读取链路上有哪些缓存、各自管什么、什么时候失效,排障思路会清晰很多。

1. 读取一次数据,ClickHouse 要过几道缓存

1.1 一条 SELECT 从发起到返回,中间经历了什么

要理解缓存,先得理清一条 SELECT 在 MergeTree 上到底怎么读数据。以最常见的SELECT sum(x) FROM t WHERE id = 123为例,大概要经过这几步:

  1. 语法解析、生成执行计划,这里不涉及磁盘。
  2. 根据分区键裁剪出要扫描的 part 列表,如果表建得好,这一步会砍掉大部分文件。
  3. 对每个 part,读取主键索引(primary.idx)做二分查找,定位到可能命中的行区间。MergeTree 的索引是稀疏索引,每index_granularity行才记录一个索引项,默认 8192 行一个。
  4. 拿到候选行区间后,需要知道这些行在列文件里具体从哪个位置开始读,这就需要读 mark 文件(.mrk/.mrk2)。
  5. 根据 mark 里的偏移量,去读.bin列文件中的压缩块。这个压缩块通常包含多个 granule 的数据。
  6. 把压缩块读进内存,解压成原始列数据,再交给后面的过滤、聚合算子处理。

你发现没有,从第 3 步开始,每一步都可能命中缓存。主键索引文件很小,基本在文件系统缓存里躺着;mark 文件有专门的内存缓存;压缩块的原始字节靠 OS 的文件页缓存;解压后的列块理论上也可以被缓存。最后,如果开了查询结果缓存,整条 SQL 的结果集还能被直接复用。

1.2 四类缓存都在缓存什么

我把读取链路上真正值得关注的缓存整理成了一张表。注意这里说的是服务端/文件系统层面的缓存,各种分布式缓存、应用层 Redis 不在讨论范围内:

缓存缓存的对象粒度管理方默认状态典型失效方式
OS Page Cache读过的磁盘文件页(含压缩块原始字节)4KB 页 / 压缩块Linux 内核开启内存回收、主动 drop_caches
Mark Cachemark 文件里的 granule 偏移信息单条 mark 记录ClickHouse 服务端开启LRU 淘汰
Uncompressed Cache解压后的列块数据单个压缩块对应的解压结果ClickHouse 服务端关闭LRU 淘汰
Query Cache最终查询结果集一条 SQL 的完整结果ClickHouse 服务端关闭TTL 到期、数据版本变化

这四类缓存的共同点是:它们都不负责"数据一致性",而是把磁盘上已经存在的字节复制一份放近处,减少重复 IO 和重复计算。

1.3 别用 Web 缓存的思维套 ClickHouse

很多从 MyBatis、Spring 三级缓存、Redis 缓存治理那套技术栈转过来的同学,会下意识问一个问题:ClickHouse 的缓存一致性怎么保证?缓存穿透怎么办?

这个问题本身就用错了模型。OLTP 系统的旁路缓存是"缓存层 + 数据库"两层结构,缓存是数据的代理,所以需要考虑一致性、穿透、击穿。ClickHouse 的读缓存则完全不同,它更像是 CPU 的 L1/L2/L3 缓存:底层是同一份数据,上层缓存只是"读过的数据块的临时副本"。它不代理写入,也不承担一致性的承诺,磁盘上的数据变了,缓存自然就失效了。想明白这一点,后面看很多调参建议就不会被带偏。

2. 被多数人忽略的 OS Page Cache:ClickHouse 最大的缓存池

2.1 为什么它比任何 ClickHouse 参数都重要

先做一个实验。你找一台内存 64GB 的服务器,部署好 ClickHouse,对一张 200GB 的表跑一个全表聚合。第一次跑可能要用 40 秒,紧接着再跑一次,可能只要 8 秒。这中间你什么都没配置,第二次快的唯一原因就是 Linux 内核把读过的文件页留在了内存里,也就是 OS Page Cache。

ClickHouse 的 MergeTree 存储设计是"压缩列存"。读数据时,实际从磁盘读出来的是压缩块的原始字节,反复读同一个 part 时,这些字节大概率已经在 page cache 里了,根本不用碰磁盘。对于 512GB 内存的分析服务器,page cache 随手就能缓存几十 GB 到上百 GB 的热数据,这个规模远超任何内部缓存配置能提供的量级。

我自己在部署 21.8.15.7 那批机器时做过一次测试:把 page cache 清掉后跑同一个报表查询,耗时从 2.8 秒涨到了 11.4 秒。也就是说,这台机器上性能的 70% 以上依赖的是文件系统缓存而不是 ClickHouse 本身的参数。很多所谓"调优"没效果,是因为你根本没意识到最大的缓存池在内核里。

所以理解 page cache 的管理机制,比调 ClickHouse 配置更基础。它占据的是服务器可用内存的一部分,内存紧张时内核会优先回收它。如果你的服务器一直在跑大查询、频繁申请内存,page cache 就可能被反复挤掉,表现出来就是查询时快时慢。

2.2 部署 21.8.15.7 时,和内核缓存相关的几个参数

如果你是照着网上的老教程部署 ClickHouse,大概率会看到一堆内核参数调整建议。我实际用下来,真正和缓存相关的就三个值得关注:

第一个是vm.swappiness。这个参数控制内核在内存紧张时是优先换出匿名内存页,还是优先回收 page cache。分析型服务器上,社区经验值一般给到 1~10。设得太高,内核可能会把一部分不常用的进程内存换到 swap,拖垮整体响应;设成 0 在某些旧内核版本上又可能触发 OOM 误杀。我习惯设成 1,配合足够大的物理内存,效果比较稳。

第二个是vm.dirty_ratio和vm.dirty_background_ratio。它管的是脏页写回策略。纯分析场景不用太动,但如果你用 Flink 持续导入数据、高频写入,默认值偏高可能导致写回集中,间接影响读取 IO。我一般把vm.dirty_background_ratio调小到 5 左右,让内核更积极地写回脏页,减少积压,但不会把vm.dirty_ratio压得过低,避免写入吞吐下降。

第三个是内核的 NUMA 策略。这个严格说不是缓存参数,但影响很大。跨 NUMA 节点访问内存时延迟会变高,page cache 命中后读取也会受影响。生产环境建议关掉自动 NUMA balancing,或者用numactl --interleave=all跑 clickhouse-server,避免内存访问不均匀。

2.3 一个我踩过的坑:不要随便 drop_caches

刚接手一套 ClickHouse 集群的时候,看到free -h显示内存被"吃掉"几百 GB,第一反应是内存泄漏。于是执行了echo 3 > /proc/sys/vm/drop_caches,瞬间"干干净净",心里还挺舒服。

结果第二天业务侧就受不了了,所有查询集体变慢,部分高并发报表直接超时。原因很简单:我手动清理 page cache 的动作,等于把服务器上几百 GB 的"热数据备份"一把火烧了。之后几天每次查询都是冷读,磁盘 IO 直接被打满,而内核本来会自动在内存和 page cache 之间做平衡,根本不需要人工干预。

后来我把这条经验固化成了运维规范:任何情况下不手动 drop_caches。如果实在要清理,必须在业务低峰期,并且要预期到接下来一段时间段查询性能会有明显回退。内存回收这种事情,Linux 内核做得比你想象中好得多,别去抢它的活。

3. Mark Cache:稀疏索引定位背后的"坐标本"

3.1 先明白 primary index、granule、mark 三者关系

很多人把 ClickHouse 主键索引想得太万能,以为是像 MySQL 那样的 B+ 树,能精确定位到每一行。实际上 MergeTree 的primary.idx非常小,只记录了每个 granule 的起始行的索引列值。它的作用是帮你快速排除掉不可能命中的 granule,而不是找到精确行。

granule 是 ClickHouse 读取的最小数据块单位,默认 8192 行。granule 和列文件之间的映射关系,就存在 mark 文件里。你可以把 mark 理解成"坐标本":它记录了这个 part 里每个 granule 在.bin列文件里的起始偏移量和压缩块大小。查询要读某个 granule 的某列时,必须先查这个坐标本,才能在列文件里找到真正要读的字节位置。

这个坐标本可能只有几十 KB,但它决定了每一次数据读取的起点。如果每次读数据前都要从磁盘重新加载 mark 文件,小 IO 会多到吓人——尤其在查询涉及大量 part 的时候。Mark Cache 就是把这个坐标本长驻在内存里,用 LRU 淘汰冷数据。

3.2 mark_cache_size 到底该调多大

Mark Cache 的大小由mark_cache_size控制,默认值大概在 5GB 左右。它是服务端全局的内存缓存,不属于单条查询的内存配额。

关于这个参数,我的观点是:绝大多数集群不需要调大,重点要看的是 part 数量而不是参数大小。为什么?

因为 mark 文件本身很小,一个 part 的 mark 文件也就是几百 KB 到几 MB 级别。默认 5GB 的缓存,能容纳的 part 数量已经相当可观。真正的问题是:当你有几万个细小 part 时,哪怕每个 part 的 mark 只有 200KB,累积起来也超过了缓存容量,热数据反而会被挤掉,此时调大mark_cache_size是治标不治本。治本是减少 part 数量——让后台 merge 跑得动,或者控制导入频率避免抖动产生大量小 part。Flink 同步 MySQL 到 ClickHouse 的场景里,持续小批量写入很容易堆出大量 part,这时候更该看的是 merge 相关的设置,而不是调 mark 缓存。

如果你一定要调整,注意mark_cache_size是消耗服务器总内存的,它会和查询内存、page cache 抢空间。在 64GB 机器上给 mark cache 硬塞 20GB,可能反而把 page cache 挤小,整体性能不升反降。

3.3 怎么观察 mark cache 的命中情况

ClickHouse 提供了几个直观的指标来观察 mark cache 状态:

SELECT metric, value FROM system.metrics WHERE metric LIKE '%MarkCache%';

这条语句能看到当前 mark cache 占了多少字节、多少文件:

SELECT event, value FROM system.events WHERE event LIKE '%MarkCache%';

这条语句看的是累计命中/未命中次数,字段包含MarkCacheHits和MarkCacheMisses。

我个人的判断标准是:单条查询要结合system.query_log里的ProfileEvents来看,只盯着服务端全局的命中率意义不大。比如一个大范围扫描的查询,mark cache miss 很多是正常的,因为本来就要读一大堆 part;但如果你发现一个点查(WHERE id = xxx)的查询,每次都有大量 mark cache miss,那就说明两种可能:要么查询裁剪后的 part 数量太多,要么 mark cache 被其他业务冲掉了。

还有一种情况容易被忽略:use_skip_indexes跳数索引的读取走的是文件系统缓存,和 mark cache 是两套机制。有人把max_mark_cache_size调大想加速 skip index,方向就错了。跳数索引读的是.idx文件,加速它要靠 page cache 容量。

4. use_uncompressed_cache:一个被误解多年的参数

4.1 它缓存的是"解压后"的数据,默认却是关的

这是 ClickHouse 里最容易让人产生误解的参数之一。字面意思很清楚:开启后,解压出来的列块数据会被放进内存 LRU,下次读取同一个压缩块时跳过解压步骤,直接拿现成的数据。

听起来很划算,但官方默认是关闭的,并且这个开关在后来的一些主干版本里干脆被移除了。为什么?因为大多数分析场景下,它的收益小于成本。

第一,LZ4 解压速度极快,解压 1MB 数据也就几十微秒级别,省下的这几十微秒,对整个查询时间影响微乎其微。第二,page cache 已经缓存了磁盘上的压缩块原文,重复读同一个压缩块时,磁盘 IO 的瓶颈已经消除了,剩下的只是"解压一次还是两次"的差别。第三,分析查询大多数是顺序扫一遍,同一个压缩块被反复读取的场景本身就不多,缓存命中率不会高。

用一个场景来类比:你去食堂打饭,米饭是现成的(压缩块),端到餐桌上需要拆开保鲜膜热一下(解压)。把热好的米饭存在保温箱里,下次再去取一份同样的米饭就能直接用。问题是,大家每次都点不同的菜,保温箱里的米饭根本没人认领,还占了操作台的地方。

4.2 什么情况下值得开一次试试

虽然默认关闭,但确实有一种场景值得测试:高频重复读取同一个较小的数据块。典型例子是反复查一个小的维度表,比如SELECT * FROM dim_area WHERE id = 42这种点查被调用几万次。

判断方法是看system.events里的两个累计事件:

SELECT event, value FROM system.events WHERE event LIKE '%Uncompressed%';

关注UncompressedCacheHits和UncompressedCacheMisses。如果 Hits / (Hits + Misses) 的比例很高,说明确实在反复命中同一个解压结果,此时开启use_uncompressed_cache = 1能带来实打实的收益。

但我要泼一盆冷水:在我见过的生产系统里,90% 的场景下这个比例都很难看。那些反复执行的报表查询,要么结果集被 query cache 接住了,要么本来就是全表扫描型的分析,不存在稳定的"热点解压块"。所以我的建议是:这个参数当作历史研究可以,不要一上来就全局开启。

4.3 这个参数还能牵出 Doris 和 ClickHouse 的选型对比

网上经常有人问 Doris 和 ClickHouse 怎么选,这个 use_uncompressed_cache 的差异其实是很好的切入点。

ClickHouse 在很长一段时间里把缓存重心放在 OS page cache 上,压缩块的解压结果默认不缓存。原因很简单:分析型查询的复用粒度太粗,解压结果大概率一次用不上第二次,让操作系统的页缓存去兜底更划算。

Doris 那边则更倾向于在 BE 进程内做自管理的页缓存(LRU),把缓存调度控制权握在自己手里,而不是完全交给内核。它的好处是缓存行为可以配置、可观测,对高并发小查询的重复命中更友好;代价是进程内存占用更重,缓存策略需要自己调。

所以如果你面临 Doris 和 ClickHouse 选型,可以从负载特征出发:如果大量是高并发、小结果集、有固定热点数据的查询,Doris 这种自管理缓存架构在命中率上通常更可预期;如果是大吞吐宽表扫描、复杂分析、对并发数不那么敏感,ClickHouse 的"让内核管缓存"路线更省心。缓存设计本质上反映了两个系统对"谁该为读放大负责"这个问题的不同回答,没有绝对优劣。

5. Query Cache:查询结果缓存到底要不要开

5.1 21.x 时代的查询缓存还比较"实验"

ClickHouse 的 Query Cache 机制在 21.x 中后期已经以实验性功能的形式存在了。它和前面的缓存完全不同:前面缓存的是数据块,它缓存的是最终查询结果集。同一个 SQL 语句再次执行时,如果命中缓存,直接返回结果,连执行计划都不重新生成。

以 21.8 这个常见的部署版本为例,相关配置大致包括use_query_cache、query_cache_ttl、query_cache_max_entries、query_cache_min_query_duration等。注意,这个功能默认是关闭的,需要显式打开。

我的态度是:它对特定业务有用,但不要把它的角色想成"数据库自带的 Redis"。它对查询模式极其挑剔,开之前必须想清楚你的报表 SQL 是否真的"重复且固定"。

5.2 适合开和坚决别开的场景

先给结论性判断:

适合开 Query Cache坚决别开
固定报表,Dashboard 每秒刷一次同一个 SQLSQL 里带now()、rand()等每次结果都不同的函数
结果集小,几百到几千行结果集几十万行,缓存内存扛不住
对数据实时性不敏感,容忍秒级延迟写入后要求立即查询到最新数据的业务
查询频次高,且完全重复高并发但每个查询条件都不同,命中率极低

我踩过一个教训:把 Query Cache 开了之后,某张表每天凌晨被 Flink 任务批量更新,白天 Dashboard 拉报表,看起来一切正常。直到有一次业务方修改了上游数据,重新跑历史分区,结果 Dashboard 还是显示旧数据——因为查询结果被缓存了,还没到 TTL。这种"看起来正常,其实是旧数据"的状态,比查询慢更危险。

5.3 Flink 持续写入场景下,我为什么一般不开

热词里有一条 "使用 Flink 实现 MySQL 同步到 ClickHouse",这正好是 Query Cache 使用中一个关键反例。

Flink 同步链路的特点是高频写入、数据持续变化。如果此时开了 Query Cache,会面临三个问题:

一是命中率。表数据一直在变,每个 SQL 下次跑的时候,可能涉及的分区已经变了,缓存条目很容易失效,实际命中率远低于预期。

二是新鲜度。即便缓存还没失效,用户查到的也是 TTL 之前的旧结果。对实时数仓来说,这是难以接受的数据质量问题。

三是内存风险。缓存条目不失效、不淘汰,就会一直堆积。配置了query_cache_max_entries后倒是不会 OOM,但大量失效条目反复写入又淘汰,会带来内存碎片和 GC 压力。

所以在 Flink 同步链路里,我的做法是:ClickHouse 这层不开 Query Cache,把"结果缓存"留给更上游的应用层去做,比如报表服务自己加一层 Redis 或本地内存缓存。这样既拿到了缓存收益,又不会牺牲查询新鲜度。

5.4 顺带说透"缓存穿透"在 ClickHouse 里的含义

每次聊缓存,总会有人提"缓存穿透"。这个概念在 Web 系统里的意思是:查询一个不存在的数据,每次都要穿透缓存打到数据库。放在 ClickHouse 里,最接近的形态是:高并发查询一个根本不存在的 key,每次都触发全表扫描或者大量索引跳转。

但 ClickHouse 里应对这个问题的思路和 Redis 完全不同。它不是靠"缓存空结果"来挡穿透的,而是靠两件事:分区裁剪和稀疏索引。你只要把分区键设计得合理,查询条件能精确落到一两个分区,就算 key 不存在,扫描的数据量也极小,根本不叫"穿透"。真正可怕的是查询条件不带分区键,那不管缓存怎么开,都是全表扫描的灾难。这种问题的解法是改表结构、改查询习惯,而不是加缓存。

6. 围绕读取缓存,我在生产环境里的调参与排障经验

6.1 一个"时快时慢"的典型排查链路

这是一次真实的排障经历。某张报表表,业务反馈查询时间忽高忽低,有时 0.5 秒,有时突然跑到 5 秒,找不到规律。

我先是查了system.query_log,对比两次执行同一 SQL 的记录,重点看read_bytes和ProfileEvents。发现一个非常明显的现象:慢查询那次的read_bytes比快查询的时候高了两个数量级。这说明慢的那次实际从磁盘读的数据量剧增,而那段时间并没有导入新数据——唯一的解释是,之前本来应该命中 page cache 的数据,没有被命中。

接着我看了system.metrics里的MarkCacheBytes,发现 mark cache 占用率并不高,排除 mark cache 问题。再查服务器内存,发现问题的根源:同一时间点,另一个团队在跑一个巨大的JOIN查询,max_memory_usage没有控制,一下子申请了大量内存。内核在内存压力下开始回收 page cache,把热点数据全挤出去了。等那个大查询跑完,page cache 还没有重新填充,于是后续查询集体冷读,性能暴跌。

整个排查链路走完,结论是:不是 ClickHouse 配置坏了,而是内存资源被抢了。后面处理方式很简单:给那个大查询套上资源限制,错峰运行,问题就消失了。

我列一下这次排障用到的关键命令和判断方法:

排查对象命令 / 视图判断逻辑
冷读 / 热读差异system.query_log查看read_bytes同一 SQL 的 read_bytes 波动大,说明缓存命中不稳定
Mark Cache 状态system.metrics的MarkCacheBytes数值接近上限且查询慢,考虑 part 过多或缓存被挤占
解压缓存命中system.events的UncompressedCacheHits比例极低时,没必要开use_uncompressed_cache
系统内存压力free -h查看 page cache 大小page cache 长期很小,说明内存在被其他东西抢
大查询内存占用system.processes的memory_usage排查哪些查询在挤压 page cache

6.2 几个真正有效的调参习惯

做完上面那次排障后,我给自己定了几条规矩:

第一,max_server_memory_usage不要设满。很多人喜欢把保留内存压到很低,觉得机器内存不用白不用。但这样一来,一旦有大查询,内核为了保证 ClickHouse 的内存请求,会不断回收 page cache,最直接的后果就是所有查询变成冷读。我一般预留 20% 左右给操作系统和 page cache,长期看整体吞吐反而更高。

第二,max_memory_usage对高并发场景要有上限。单条查询可以宽松,但并发总和必须受控。最好的工具是system.processes里看当前内存占用,一旦发现某个查询吃了近一半内存,立刻评估是不是要限流。

第三,关注 part 数量,而不是只调各种 cache size。前面反复提到,part 多了,mark 文件总量就大,缓存压力自然上去。定期查看system.parts里每个分区的 part 数量,如果持续积累,优先排查 merge 配置,而不是加缓存内存。

第四,压测时一定要做"缓存区分"测试。连续跑同一查询 5 次,记录第一次和第五次的差异。如果差异巨大,说明系统整体依赖缓存,你要回答的问题是"缓存被挤掉后,系统还能不能扛住业务峰值"。

6.3 缓存调优的本质

一路看下来你会发现,ClickHouse 的读取缓存调优核心其实不在 ClickHouse 参数里,而在内存资源的分配格局上。

四类缓存里,page cache 体量最大,完全由内核控制;mark cache 体量中等,由 ClickHouse 控制,但占用的是同一块内存池;uncompressed cache 默认不开,开了也只是用内存换解压时间;query cache 是业务侧的可选项,用内存换查询重复执行的成本。

它们之间存在一个朴素的竞争关系:所有缓存都在抢内存,内存总量是固定的,此消彼长。网上那些"把各种 cache size 都调大"的教程,基本都是没有考虑这种竞争关系。真正合理的分配是:让 page cache 有足够空间,mark cache 保持默认,uncompressed cache 不开,query cache 按业务精确开关。

我在实际运营中还有个体会:很多"缓存问题"其实是表结构问题。比如查询条件永远不带分区键,导致扫描量巨大;比如字段太多、SELECT *满天飞,导致 read 放大;比如索引设计不合理,导致稀疏索引定位不到几个 granule。这些问题不管缓存开多大,都只是临时缓解,最终还是要回到数据模型本身来解决。

最后再分享一个小技巧:排查缓存问题时,先做一个对照实验——重启服务或者清掉 page cache 前后,跑同一个查询看耗时变化。如果清掉后性能暴跌,说明你的负载很依赖缓存,这时候该考虑的是内存容量够不够、资源分配合不合理;如果清掉后性能几乎不变,说明查询本身就在冷读,加缓存参数的收益也微乎其微,该去优化索引和分区裁剪。

下篇我会接着聊写入侧的缓存行为、后台 merge 如何破坏缓存热度,以及字典缓存和元数据缓存的坑。如果你在项目里也遇到过"查询时快时慢"的诡异现象,建议先把今天这套排查链路走一遍,很大概率能定位到真正的原因。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/6 9:37:07

DRV8825实战避坑指南:电源、地线、REF校准与衰减模式深度解析

1. 为什么DRV8825不是“插上就能转”的黑盒子——从一块烧毁的电机板说起去年调试一台CNC雕刻机时,我亲手把三块DRV8825模块全烧成了焦黑色。不是因为电压超限,也不是接反了电源,而是——我把细分拨码开关调到了1/32,却用Arduino …

作者头像 李华
网站建设 2026/10/6 9:33:41

Java BigDecimal 精度避坑指南:从浮点数误差到实战应用

开头要写得像有经验的Java开发者在分享经验。 我做过多个金融相关的项目,每次接手和金额有关的模块,都要先看一眼代码里用的是 double 还是 BigDecimal。这个习惯的由来,是一次线上事故——一个账务系统用 double 算利息,季度结算…

作者头像 李华
网站建设 2026/10/6 9:33:37

Dify与RAG融合架构:行业问答机器人落地与生产部署实践

简介:面向具备Python与Web开发基础、正在从事AI智能体或知识库问答系统研发的中级开发者,这份PDF系统讲解了基于Dify与RAG融合架构的行业问答机器人构建方案,覆盖智能体工作流设计与生产级部署全链路。内容从智能体架构全景图出发&#xff0c…

作者头像 李华
网站建设 2026/10/6 9:32:48

基于Unity3D的交通标识科普问答系统开发实践

去年参与社区交通安全科普活动的时候,我负责给十几名小学生讲解交通标识。拿着PPT讲了半个多小时,孩子们记住的大概只剩一句“红灯停绿灯行”,散场之后只有一个孩子跑来问“那个三角形的牌子到底是干嘛的”。当时没能给出一个让他满意的回答&…

作者头像 李华
网站建设 2026/10/6 9:31:35

OpenShell:模块化Shell增强方案,大幅提升终端操作效率

OpenShell是我在过去半年里反复打磨的一套终端环境增强方案,核心目标只有一个:让命令行操作变得更顺滑、更可复用、更不容易出错。它不是某一个小工具,也不是某个炫酷的主题,而是一整套围绕Shell的配置集合,覆盖了终端…

作者头像 李华
网站建设 2026/10/6 9:31:18

主从博弈与共享储能:综合能源微网双层优化建模与求解实践

这个项目做下来,最深的感受是“主从博弈共享储能综合能源微网”这三个词拆开看都不算新概念,但把它们拧在一起,就会逼你把商业模式、物理模型和算法实现全部重新捋一遍。这篇文章我就直接把这套东西摊开讲,从为什么选这个框架&…

作者头像 李华