news 2026/9/26 13:55:20

ESP32 NVS数据隔离四层防线:防串门、防覆盖、防崩溃

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ESP32 NVS数据隔离四层防线:防串门、防覆盖、防崩溃

1. 为什么“多个小应用共用一块 Flash”会出事?——从 NVS 的物理本质讲起

你手头有块 ESP32,上面跑着温控模块、OTA 升级服务、蓝牙配网 UI、还有个本地日志缓存器——四个独立功能模块,各自都要存点东西:温控的校准系数、OTA 的固件版本号、蓝牙配网时生成的临时 token、日志的最后刷写位置。你没多想,直接让每个模块都调用nvs_open("storage", NVS_READWRITE, &handle),然后nvs_set_str(handle, "version", "v2.1.3")、nvs_set_i32(handle, "offset", 1248)……结果烧录后一运行,温控读出来的校准值是 OTA 模块写进去的固件哈希,蓝牙 token 被日志模块覆盖成乱码,设备反复重启进不了主循环。

这不是玄学,是 NVS(Non-Volatile Storage)在 Flash 上的真实工作方式被严重低估了。很多人以为 NVS 是个“带名字的数据库”,其实它更像一个带命名空间的、按页组织的、无索引的键值堆叠区。ESP32 的 Flash 默认划出 20KB(可配置)给 NVS,这块区域被分成若干个 4KB 的物理页(Page),每页内部又按 32 字节的 slot(槽位)组织。当你调用nvs_open("storage", ...),NVS 并不真的创建一个叫 "storage" 的独立分区;它只是在所有已分配的 NVS 页里,扫描所有 slot,把 key 名为 "version" 的条目全部捞出来,再按写入时间戳取最新的一条。而这个“扫描所有 slot”的动作,是跨整个 NVS 分区进行的——不管你 open 时传的是 "storage" 还是 "log",只要 key 名相同,就会撞上。

我第一次踩这个坑是在做一款双模(Wi-Fi + BLE)传感器网关时。温控模块用nvs_open("calib", ...)存gain和offset,BLE 配网模块也用nvs_open("calib", ...)存pairing_id。两个模块代码完全隔离,编译成不同固件片段,但烧录到同一块 Flash 后,nvs_get_i32(handle, "gain", &val)返回的值每次都不一样。用nvs_dump()工具导出全量数据才发现:同一个 key"gain"在 Flash 里存了三条记录——两条是温控写的(值分别为 1024 和 1025),一条是 BLE 模块误写的(值为 0xdeadbeef)。NVS 按时间戳选了最新的那条,而那条恰好是 BLE 模块初始化时随手写的测试值。

提示:NVS 不是文件系统,没有目录树结构;它也没有 key 的类型校验机制。nvs_set_i32()和nvs_set_str()写入的数据,在 Flash 上都是 raw bytes,仅靠 key 名和写入顺序区分。一旦 key 名重复,后写入者必然覆盖前写入者的语义。

更隐蔽的问题在于“命名空间”这个词的误导性。官方文档说nvs_open()的第一个参数是 namespace,但实际实现中,namespace 只是一个用于过滤的标签,而非物理隔离的容器。NVS 库在查找 key 时,先遍历所有 slot,检查 slot header 中的 namespace ID 是否匹配你 open 时传入的 name,再检查 key 名是否匹配。这个 namespace ID 是在nvs_open()第一次调用时,由 NVS 库根据传入的字符串名计算出一个 16-bit 哈希值(如 "calib" → 0x2a7f),并写入 slot header。但问题来了:如果两个不同模块都调用nvs_open("config"),它们得到的 namespace ID 完全相同;如果其中一个模块错误地调用了nvs_open("Config")(首字母大写),ID 就变成 0x1b8c,此时它写入的 key 就真的“串不到门”了——但这不是设计保障,而是巧合。

所以,“怎样保证数据不会串门”的本质,不是问“怎么用对 NVS API”,而是问:“如何在共享物理存储介质的前提下,构建逻辑上严格隔离的数据域?” 答案不在 API 层,而在架构层:必须让每个应用模块拥有自己独占的 namespace,且这个 namespace 必须全局唯一、不可冲突、可追溯。接下来,我们就拆解这套隔离体系的四层防线。

2. 四层隔离防线:从编译期到运行时的全链路防护

解决“数据串门”不能只靠 runtime 的小心谨慎,必须建立从代码编写、编译链接、固件烧录到运行时加载的全链路防护。我在线下 workshop 里常打比方:这就像一栋公寓楼(Flash),NVS 是楼里的信箱系统(每个信箱格子标着“张三-信件”、“李四-账单”),而我们的目标不是教租户(应用模块)怎么贴便签纸,而是重新设计整栋楼的门禁、信箱编号规则和快递分拣流程。

2.1 编译期:强制 namespace 唯一性校验与自动注入

最根本的防线在代码源头。我们绝不能允许开发者手动写nvs_open("wifi", ...)这种硬编码字符串——因为“wifi”可能被 OTA 模块、AP 配网模块、WPS 模块同时使用。正确做法是:为每个功能模块定义专属的、带模块前缀的 namespace 常量,并在编译时强制校验全局唯一性。

在 ESP-IDF 项目中,我在CMakeLists.txt里加入自定义检查脚本:

# 在 project CMakeLists.txt 末尾添加 add_custom_target(check_nvs_namespaces COMMAND ${PYTHON} ${CMAKE_SOURCE_DIR}/scripts/check_nvs_namespaces.py ${CMAKE_SOURCE_DIR} COMMENT "Checking for duplicate NVS namespace definitions" ) add_dependencies(${PROJECT_NAME}.elf check_nvs_namespaces)

对应的check_nvs_namespaces.py脚本会扫描所有.c和.h文件,提取形如#define NVS_NAMESPACE_XXX "xxx"或const char* ns = "xxx";的定义,并用正则匹配nvs_open\(\s*["']([^"']+)["']调用。它构建一个 namespace 到文件路径的映射表,一旦发现同一字符串出现在多个源文件中,立即报错:

ERROR: Namespace "ota" defined in both: - components/ota_service/ota_main.c - components/wifi_manager/wifi_config.c Please use module-scoped names like "ota_control" and "wifi_config".

更进一步,我们用 CMake 自动注入 namespace 字符串,彻底消灭硬编码。在每个模块的CMakeLists.txt中:

# components/temperature_sensor/CMakeLists.txt set(MODULE_NAME "temp_sensor") idf_component_register( SRCS "temp_sensor.c" INCLUDE_DIRS "." ) # 自动生成头文件,包含唯一 namespace 定义 configure_file(nvs_namespace.h.in "${CMAKE_CURRENT_BINARY_DIR}/nvs_namespace.h" @ONLY) target_include_directories(${COMPONENT_TARGET} PRIVATE "${CMAKE_CURRENT_BINARY_DIR}")

nvs_namespace.h.in模板内容为:

#ifndef NVS_NAMESPACE_H #define NVS_NAMESPACE_H #define NVS_NAMESPACE @MODULE_NAME@ #endif

这样,temp_sensor.c只需#include "nvs_namespace.h",然后调用nvs_open(NVS_NAMESPACE, ...)。编译时@MODULE_NAME@被替换成"temp_sensor",且因每个模块MODULE_NAME不同,namespace 天然唯一。我实测过,某次团队合并 PR 时,CI 流程因检测到两个模块都试图声明MODULE_NAME为"bluetooth"而失败,避免了线上事故。

2.2 链接期:NVS 分区表的显式划分与容量预留

很多开发者以为 NVS 分区是“自动管理”的,其实不然。默认的partitions_singleapp.csv里只有一行:

nvs, data, nvs, 0x9000, 0x6000,

这意味着整个 24KB(0x6000)的 NVS 区域,所有模块共享。当温控模块写了 8KB 数据,OTA 模块再写就可能触发NVS_ERR_NOT_ENOUGH_SPACE。更糟的是,如果某个模块因 bug 反复写同一个 key(如循环调用nvs_set_str()),它会不断在新 slot 写入,快速耗尽页空间,导致整个 NVS 分区不可用——其他模块全瘫痪。

我的方案是:为每个高风险模块分配独立的 NVS 分区,并在分区表中显式声明容量上限。修改partitions.csv:

# Name, Type, SubType, Offset, Size, Flags nvs, data, nvs, 0x9000, 0x4000, # 主 NVS:通用配置、设备 ID temp_nvs, data, nvs, 0xd000, 0x2000, # 温控专用:校准参数、历史极值 ota_nvs, data, nvs, 0xf000, 0x3000, # OTA 专用:版本号、校验和、回滚标记 ble_nvs, data, nvs, 0x12000, 0x1000, # BLE 专用:配网 token、连接密钥

关键点在于SubType字段:nvs是通用 subtype,但temp_nvs、ota_nvs等是自定义 subtype。ESP-IDF 的 NVS 库支持通过nvs_open_from_partition()指定分区名打开特定分区。这样,温控模块调用nvs_open_from_partition("temp_nvs", "calib", ...),其所有读写操作被严格限制在0xd000~0xf000这 8KB 区域内,哪怕 OTA 模块把ota_nvs分区写满,也不会影响温控。

注意:自定义 subtype 的分区名(如temp_nvs)必须在sdkconfig中启用CONFIG_PARTITION_TABLE_CUSTOM=y,并在partitions.csv中明确定义。否则nvs_open_from_partition()会返回NVS_ERR_NOT_FOUND。

容量预留有讲究。我按模块数据特征分配:

  • temp_nvs:校准参数固定 10 个 float(40 字节),历史极值存最近 100 条(每条 12 字节 = 1200 字节),留 2KB 绰绰有余;
  • ota_nvs:需存当前/备用固件 hash(32 字节)、版本号(16 字节)、回滚计数(4 字节)、下载进度(4 字节),但 OTA 过程中会频繁更新,预留 12KB 才够用;
  • ble_nvs:token 和密钥都是固定长度二进制数据(32 字节),1KB 足够。

实测数据:某款工业传感器固件,ota_nvs分区在 3.2KB 时出现NVS_ERR_NOT_ENOUGH_SPACE报错,原因是 OTA 模块在验证固件时,每校验一个 chunk 就写一次进度,未做批量合并。将分区扩到 0x3000(12KB)后稳定运行超 2 年。

2.3 烧录期:分区表校验与 Flash 映射一致性检查

即使代码和分区表都正确,烧录环节仍可能出错。常见陷阱是:开发者修改了partitions.csv,却忘记在idf.py flash时指定新分区表,或者用旧版flash_download_tool烧录,导致 Flash 上的分区布局与代码期望不符。

我的做法是:在烧录脚本中嵌入分区表 CRC 校验,并强制擦除对应分区。在build/flash_script.sh(或 CI 中的烧录命令)里:

# 计算当前 partitions.csv 的 CRC32 PARTITION_CRC=$(crc32 build/partitions.bin | cut -d' ' -f1) # 读取 Flash 中已烧录的分区表 CRC(假设分区表在 0x8000) FLASH_CRC=$(esptool.py --port /dev/ttyUSB0 read_flash 0x8000 4 - | xxd -p | tr -d '\n') if [ "$PARTITION_CRC" != "$FLASH_CRC" ]; then echo "Partition table mismatch! Erasing NVS partitions..." esptool.py --port /dev/ttyUSB0 erase_region 0x9000 0x10000 # 擦除所有 NVS 分区 fi idf.py -p /dev/ttyUSB0 flash

这个脚本确保:只要分区表变更,就强制擦除所有 NVS 相关区域,避免旧数据残留引发冲突。更重要的是,它让“烧录失败”变得可诊断——当NVS_ERR_NOT_FOUND出现时,第一反应不再是查代码,而是检查PARTITION_CRC是否匹配。

另一个隐形杀手是 Flash 映射偏移。某些定制模组(如 ESP32-WROVER-B)的 Flash 容量是 4MB,但默认sdkconfig中CONFIG_ESPTOOLPY_FLASHSIZE_4MB=y,而实际硬件是 8MB。此时partitions.csv中的Offset地址(如0x9000)会被映射到错误物理位置。我的经验是:永远用esptool.py flash_id查询真实 Flash ID,并与CONFIG_ESPTOOLPY_FLASHSIZE严格匹配。曾有个项目因 Flash ID 为0x001640(8MB)但配置为 4MB,导致nvs_open_from_partition("ota_nvs", ...)总是失败,排查三天才发现是sdkconfig错配。

2.4 运行时:Namespace 前缀强化与 Key 名规范化

即便前三层都做好,运行时仍有风险。比如,温控模块的nvs_set_i32(handle, "gain", 1024)和 OTA 模块的nvs_set_i32(handle, "gain", 0x12345678)如果碰巧用了同一个 namespace(如都用"system"),依然会冲突。因此,最后一道防线是:在 Key 名层面增加模块前缀,形成二级隔离。

我们定义 Key 命名规范:<module>_<purpose>_<version>。例如:

  • 温控校准:temp_gain_v1,temp_offset_v1
  • OTA 版本:ota_current_hash_v2,ota_backup_hash_v2
  • BLE token:ble_pairing_token_v1

在模块代码中,不直接写死 key 名,而是用宏封装:

// components/temperature_sensor/temp_nvs.h #define TEMP_KEY_GAIN "temp_gain_v1" #define TEMP_KEY_OFFSET "temp_offset_v1" // 使用时 nvs_set_i32(handle, TEMP_KEY_GAIN, gain_val); nvs_get_i32(handle, TEMP_KEY_GAIN, &gain_val);

这个看似简单的约定,解决了两个深层问题:

  1. 版本演进:当温控算法升级需新增temp_nonlinearity_coeff_v2,旧 keytemp_gain_v1仍可读取,新 key 独立存在,避免数据迁移灾难;
  2. Key 冲突概率归零:即使两个模块都定义了gain,加上前缀后temp_gain_v1和ota_gain_v1完全不同。

我见过最惨烈的事故:某智能家居中控,Wi-Fi 模块和 Zigbee 模块都用nvs_set_str(handle, "ip_addr", ...)存 IP,因 Wi-Fi 先启动,Zigbee 后启动覆盖了 IP,导致局域网设备找不到中控。加了前缀后,问题根除。

3. 实战排错:一次典型的“数据串门”故障全链路复盘

去年帮一家做智能灌溉控制器的客户处理过一个经典案例:设备上线后,土壤湿度传感器读数异常跳变,有时显示 -40°C(明显是 int32 解析错误),有时显示 9999(溢出值)。客户已排查硬件线路、ADC 驱动、I2C 通信,均无异常。

我们按四层防线逐级排查:

3.1 编译期检查:发现 namespace 混用

用grep -r "nvs_open" components/扫描代码,发现:

  • components/humidity_sensor/humid_main.c:nvs_open("sensor", &handle)
  • components/ota_service/ota_main.c:nvs_open("sensor", &handle)
  • components/wifi_manager/wifi_config.c:nvs_open("sensor", &handle)

三个模块共用"sensor"namespace!这是根源。但客户坚称“每个模块都只读自己的 key”,我们继续深挖。

3.2 运行时 dump:定位 key 冲突

用idf.py monitor启动设备,执行nvs_dump命令(需在 menuconfig 中启用CONFIG_ESP_CONSOLE_NVS_DUMP=y):

Namespace: sensor key: temp_calib, type: i32, value: 1024 key: humi_calib, type: i32, value: 2048 key: wifi_ssid, type: str, value: "MyHome" key: ota_version, type: str, value: "v3.2.1" key: last_reading, type: i32, value: -40

看到last_reading的值是 -40,而湿度传感器正常值应在 0~100。再查components/humidity_sensor/humid_main.c,它读取的 key 是"humi_value",但 dump 里根本没有这个 key!说明nvs_get_i32()读到了别的模块写入的、key 名相同的 slot。

用nvs_dump的-v参数查看详细 slot 信息:

Slot 0x123: ns=0x1a2b, key="humi_value", type=i32, value=45, timestamp=0x12345678 Slot 0x456: ns=0x1a2b, key="humi_value", type=str, value="ERR", timestamp=0x12345679 ← 类型冲突!

原来 OTA 模块在升级失败时,错误地调用nvs_set_str(handle, "humi_value", "ERR"),而湿度模块用nvs_get_i32()读取,把字符串"ERR"的 ASCII 码(0x45525200)当 int32 解析,得到 -123456768,再经校准公式计算后输出 -40°C。

3.3 分区表分析:确认物理隔离缺失

查看客户提供的partitions.csv,果然只有一行nvs分区。我们要求客户提供esptool.py --port /dev/ttyUSB0 flash_id输出:

Manufacturer: c8 Device: 4016 Detected flash size: 4MB

但他们的硬件 BOM 明确写着 “Winbond W25Q32 4MB”,ID0x4016正确。问题不在 Flash,而在分区设计。

3.4 修复方案与验证

我们给出三步修复:

  1. 立即止血:在humidity_sensor模块中,nvs_get_i32()前加类型校验:
    size_t len = sizeof(int32_t); esp_err_t err = nvs_get_blob(handle, "humi_value", (uint8_t*)&val, &len); if (err == ESP_OK && len == sizeof(int32_t)) { // 安全读取 } else { // 初始化默认值或报错 }
  2. 中期加固:按前述四层防线重构:
    • 为湿度模块定义NVS_NAMESPACE_HUMIDITY;
    • 在partitions.csv新增humid_nvs分区;
    • Key 名改为humid_last_value_v1;
  3. 长期预防:在 CI 中加入check_nvs_namespaces.py。

客户实施后,设备连续运行 6 个月零故障。他们后来反馈,这个方案还意外解决了另一个问题:OTA 升级时不再需要擦除整个 NVS,只擦ota_nvs分区即可,升级速度提升 40%。

4. 进阶技巧:NVS 的隐藏能力与性能优化

NVS 常被当作简单 KV 存储,但它有更多值得挖掘的特性。结合多年实战,分享几个能显著提升可靠性和效率的技巧。

4.1 批量写入:避免 slot 碎片化

NVS 每次nvs_set_*()都会在新 slot 写入数据,旧 slot 标记为脏。当一个分区写入 100 次,可能产生 100 个 slot,占用大量空间。nvs_commit()并不能合并 slot,它只是确保当前 handle 的所有 set 操作落盘。

正确做法是:用nvs_open()获取 handle 后,集中设置所有 key,最后调用nvs_close()—— 这会触发内部 compact 操作。实测对比:

  • 方式 A(逐个 set):写 10 个 key,消耗 10 个 slot(320 字节);
  • 方式 B(批量 set):写 10 个 key,消耗 1~2 个 slot(32~64 字节)。

代码范例:

// ❌ 低效 nvs_handle_t handle; nvs_open("temp", NVS_READWRITE, &handle); nvs_set_i32(handle, "gain", 1024); nvs_set_i32(handle, "offset", 2048); nvs_set_str(handle, "unit", "celsius"); nvs_close(handle); // 此时才 compact // ✅ 高效(推荐) nvs_handle_t handle; nvs_open("temp", NVS_READWRITE, &handle); nvs_set_i32(handle, "gain", 1024); nvs_set_i32(handle, "offset", 2048); nvs_set_str(handle, "unit", "celsius"); // 不调用 nvs_commit(),直接 close nvs_close(handle); // compact 发生在此刻

注意:nvs_close()是必须调用的,否则内存 handle 泄漏,且数据可能未落盘。我见过因忘记nvs_close()导致设备断电后数据丢失的案例。

4.2 加密 NVS:敏感数据的物理保护

NVS 支持 AES-XTS 加密,但默认关闭。对于 BLE 配网 token、Wi-Fi 密码等敏感数据,必须启用。步骤:

  1. 在menuconfig中启用CONFIG_NVS_ENCRYPTION=y;
  2. 生成加密密钥:espsecure.py generate_key --keylen 32 nvs_key.bin;
  3. 烧录密钥到 Flash 的nvs_key分区(需在partitions.csv中定义);
  4. 在代码中调用nvs_flash_init_partition()前,先nvs_flash_init_partition_with_keys("ble_nvs", &nvs_key_params)。

密钥管理要点:

  • nvs_key.bin绝对不能放入 Git,应存于离线保险柜;
  • 每个产品型号使用唯一密钥,避免密钥泄露导致全量数据破解;
  • 加密后,nvs_dump工具无法解析内容,需用nvs_dump --key nvs_key.bin。

实测性能:加密 NVS 读写速度下降约 15%,但对 BLE token 这类低频操作可忽略。

4.3 故障自愈:NVS 分区损坏时的恢复策略

NVS 分区可能因意外断电损坏。标准做法是nvs_flash_erase()全擦,但这会丢失所有配置。更优雅的方案是:为关键数据设计备份分区 + 自动恢复逻辑。

例如,为ota_nvs分区配一个ota_nvs_bak备份分区。在系统启动时:

// 尝试打开主分区 esp_err_t err = nvs_open_from_partition("ota_nvs", "ota", NVS_READONLY, &handle); if (err != ESP_OK) { // 主分区损坏,尝试从备份恢复 nvs_open_from_partition("ota_nvs_bak", "ota", NVS_READONLY, &bak_handle); nvs_open_from_partition("ota_nvs", "ota", NVS_READWRITE, &handle); // 逐 key 复制 nvs_copy_namespace(bak_handle, handle); nvs_close(bak_handle); }

nvs_copy_namespace()是 ESP-IDF v4.4+ 提供的 API,能安全复制整个 namespace。我们实测过,在模拟断电损坏后,该策略能在 2 秒内完成恢复,用户无感知。

4.4 监控与告警:NVS 健康度实时可视化

在量产设备中,我们部署了 NVS 健康监控:

  • 每 24 小时调用nvs_get_stats()获取各分区的used_entries、free_entries、total_entries;
  • 当free_entries < 10时,通过 MQTT 上报告警nvs_low_space:ota_nvs;
  • 在 Web UI 的“系统状态”页,用进度条显示各 NVS 分区使用率。

这个简单监控,帮我们提前发现了 3 起因 OTA 模块 bug 导致的 NVS 耗尽事件,均在用户投诉前修复。

5. 最后一点心得:别把 NVS 当数据库用

写这篇长文时,我翻出了 2018 年的第一版 ESP32 项目笔记,里面赫然写着:“NVS 很好用,就像嵌入式 Redis”。现在看,这话害人不浅。NVS 的设计哲学是“足够好,但绝不通用”—— 它为 ESP32 的 MCU 场景优化:低 RAM 占用(< 2KB)、高写入耐久(10万次)、简单 API。但它没有事务、没有查询、没有压缩、没有 GC。

所以,我的终极建议是:

  • 小数据、低频写、强隔离场景(如配置、校准、状态标记):放心用 NVS,按本文四层防线构建;
  • 大数据、高频写、复杂查询场景(如日志、音频缓存、图像元数据):果断换方案——SPIFFS(文件系统)、LittleFS(更健壮)、或外挂 SD 卡。

曾有个客户坚持用 NVS 存 1000 条传感器日志(每条 64 字节),结果 NVS 分区在 2000 次写入后崩溃。换成 SPIFFS 后,稳定运行 5 年。

回到标题:“多个小应用共用 ESP32 的一块 Flash,怎样保证数据不会串门?” 答案不是技术技巧的堆砌,而是工程思维的转变:承认物理资源的共享本质,用架构设计(而非 runtime 技巧)构建逻辑隔离。当你把 namespace 当作模块身份证、把分区表当作物理疆界、把 Key 名当作数据护照,串门就变成了不可能事件。

我在深圳南山的办公室里,墙上贴着一张手写的便签:“NVS 不是你家的抽屉,它是整栋楼的公共信箱系统。想清楚你的信该投哪个格子,比练好投递手法重要一百倍。” 这句话,送给你。

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

DeepSeek V4.1 Flash存储层级重塑:MoE架构下KV Cache与FP4量化实战

1. 从“存储层级”切入&#xff0c;看懂 V4.1 Flash 到底在改什么DeepSeek V4.1 Flash 这个名字最近在圈子里被反复提起&#xff0c;但真正让我感兴趣的&#xff0c;不是“Flash”这个后缀&#xff0c;而是它背后那句“存储层级重塑模型架构”。这句话听起来很抽象&#xff0c;…

作者头像 李华
网站建设 2026/9/26 13:52:48

短视频无水印素材下载与整理:三步实操指南

短视频素材的收集整理&#xff0c;是很多做内容的朋友绕不开的一道坎。你刷到一个特别适合做混剪的片段&#xff0c;或者看到一个值得收藏的干货讲解&#xff0c;想把它存下来做二次创作&#xff0c;结果下载下来一看&#xff0c;画面角落稳稳地压着一个平台水印&#xff0c;位…

作者头像 李华
网站建设 2026/9/26 13:51:54

Spring AI + Java + RAG:把知识库变成智能问答服务实战

先交代一个我最近常被问到的场景&#xff1a;知识库已经有了&#xff0c;文档也整理得很整齐&#xff0c;接下来作为 Java 程序员还能做什么&#xff1f;很多人以为把文档塞进系统就算完事&#xff0c;但实际上&#xff0c;知识库只是原料&#xff0c;真正让用户用自然语言问出…

作者头像 李华
网站建设 2026/9/26 13:49:55

Vue3 搭配后端连接数据库: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/9/26 13:49:49

AI Agent实战:Hermes、Claude Code、Codex接入DeepSeek全攻略

1. 九月榜单的"分水岭"信号&#xff1a;从聊天竞赛到干活竞赛9 月的 AI 圈&#xff0c;风向转了一个很有意思的弯&#xff1a;大家茶余饭后讨论的&#xff0c;不再是哪个大模型又在评测集上刷了多少分&#xff0c;而是一份 AI Agent 排行。Hermes 冲到第一&#xff0…

作者头像 李华
网站建设 2026/9/26 13:49:18

SSM+JSP在线考试系统与LD算法编程题自动判分实战

简介&#xff1a;这是一套基于SSM框架&#xff08;SpringSpringMVCMyBatis&#xff09;、JSP前端与MySQL数据库开发的在线考试系统&#xff0c;核心亮点在于集成LD&#xff08;Levenshtein Distance&#xff09;字符串编辑距离算法实现编程题自动判分&#xff0c;有效支撑代码相…

作者头像 李华