在 16G 内存的 Windows 本机跑通 shopkeeper-agent:13 个坑的完整排障笔记

跑了整整两天,才把 shopkeeper-agent 这个 AI 电商问数项目在本机完整跑通——从 uv sync 报错,到 LangChain JSON 解析器被 LLM 的思考块炸掉,再到 Docker Desktop 弹窗黑屏。这篇博客把我踩到的 13 个坑按”问题 → 根因 → 解决”的节奏整理成一条故事线,每一步都附可复现的命令。

如果你也在 Windows + Python 3.14 + Docker + 国内网络这个”地狱四件套”下跑 AI 项目,这篇应该能帮你少走两天弯路。

🎧 文章导读

🎵 背景音乐

前言:为什么要在本机跑这个项目

shopkeeper-agent 是一个 Text-to-SQL 的电商问数 Agent:你用自然语言问”统计华北地区的销售总额”,它会拆解意图、查元数据知识库、检索字段、生成 SQL、执行、返回结果。整套链路是当下最典型的 RAG + Agent 范式——LLM 负责推理,向量库做语义召回,关系库做事实落地。

但官方文档在 macOS / Linux 上跑得很顺,到了 Windows 这边几乎每一步都炸。原因很简单:项目用 Python 3.14(最新)、asyncmy(Cython 扩展,需要 MSVC 编译)、HF 下载(国内网络不稳)、Docker Desktop 4 容器(16G 内存吃紧)。每一项单独看都不致命,叠加在一起就是连环炸。

我把这 13 个坑按”环境 → 依赖 → 中间件 → 后端 → 模型”的顺序重新组织,读起来更像调试日志,而不是 dump 出来的报错清单。

shopkeeper-agent 系统架构图
图 1:shopkeeper-agent 整体架构——FastAPI 后端调度 LLM + 4 个 Docker 中间件

环境速览

先交代舞台。下表是我跑通时的全套环境配置(版本错一位,行为就可能不一样):

组件 版本 / 配置 备注
OS Windows 11 Home China(10.0.22631) WSL2 + Docker Desktop
内存 16 GB 紧张,但够用
Python 3.14.3(uv 管理) 最新版,少数包无 wheel
uv 0.10.9 比 pip 快得多,但会扫祖先目录
Docker Desktop 28.1.1(WSL2 backend) 存储迁到 E:\dockerimgerdeful\DockerDesktopWSL
Docker Compose v2.35.1 docker compose(不是 docker-compose
Node 14.21.3 → 22.20.0 Node 14 太老,前端起不来,必须切
LLM MiniMax-M3(OpenAI 兼容) extra_body 关 think 输出
Embedding BAAI/bge-large-zh-v1.5 ModelScope 国内源

第一章:Python 环境与依赖——两个温柔的陷阱

坑 1:uv 扫祖先目录,把外层 pyproject.toml 拉下水

1
2
3
4
uv python install 3.14
cd shopkeeper-agent-main
uv python pin 3.14
uv sync

最后一步炸了:

1
2
Failed to parse: ai-application/pyproject.toml
project.version field is neither set

第一反应是 shopkeeper-agent-main/pyproject.toml 写错了,但真正的问题是外层仓库。我的项目路径是这样的:

1
2
3
4
F:\bug图\zhioai\ai-application\ai-application\
└── 源码项目\
└── shopkeeper-agent-main\
└── pyproject.toml ← 我以为只看这个

ai-application/pyproject.toml(仓库的”骨架”,教程用的)也有一个 [project] 段,但漏写了 version 字段。uv 0.10 的策略是”沿祖先目录扫所有 pyproject.toml“,PEP 621 又强制 project.version 必须存在,于是解析阶段直接噎死。

解决:外层 ai-application/pyproject.toml 补一行 version = "0.0.0"。一行配置,绕过一个 PEP 621 校验。

💡 教训:用 uv 的同学,别把项目放在”教程仓库”里。要么把外层 pyproject.toml 改合规,要么干脆 cd 进自己的目录再开 terminal。

坑 2:asyncmy 编译——Windows 无 MSVC 的死结

uv sync 接着报:

1
asyncmy==0.2.11 ... Microsoft Visual C++ 14.0 or greater is required

asyncmy 是 Cython 扩展,需要在本地编译出 .pyd。Windows 默认没装 MSVC Build Tools,PyPI 上又没有 Python 3.14 的预编译 wheel——3.14 太新了,几乎所有 C 扩展都得自编译。

我有三个选择:装 MSVC(10G+、耗时长)、装 VS Build Tools(一样重)、换驱动。考虑到这只是数据库驱动,纯 Python 的 aiomysql(基于 PyMySQL)完全可以胜任,最终选了第三条路。

改动清单

1
2
3
# shopkeeper-agent-main/pyproject.toml
- "asyncmy>=0.2.11"
+ "aiomysql>=0.2.0"
1
2
3
# app/clients/mysql_client_manager.py
- mysql+asyncmy://
+ mysql+aiomysql://

一共改了 3 处连接串 + 几行注释,119 个依赖顺利装好。

第二章:LLM 接入——OpenAI 兼容接口的甜与苦

shopkeeper-agent 用 LangChain 的 init_chat_model(model_provider="openai"),这意味着任何 OpenAI 兼容 provider 都能直接接——只要改 base_urlapi_key

我先按官方文档配 MiniMax:

1
2
3
4
5
# conf/app_config.yaml
llm:
model_name: MiniMax-M3
api_key: ${oc.env:LLM_API_KEY}
base_url: https://api.minimaxi.com/v1
1
2
# .env
LLM_API_KEY=sk-cp-...

启动、调用,看起来一切正常,直到 LangChain 的 JSON parser 报错。这就是后面会单独讲的”坑 12”——推理模型的思考块输出和 JSON 解析器的兼容性。这章先按住不表,因为它是 13 个坑里最值得单独立传的一个

第三章:Embedding 模型——国内网络的两条路

坑 3:hf-mirror 看起来配了,其实没生效

1
HF_ENDPOINT=https://hf-mirror.com uvx --from huggingface_huggingface_cli ...

报错:

1
LocalEntryNotFoundError / FileMetadataError

环境变量确认生效了,但下载依然失败。hf-mirror 维护方在 GitHub Issues 答复过——部分大模型(bge-large-zh-v1.5 这种热门模型)的镜像同步经常滞后

ModelScope(阿里达摩院,国内稳定):

1
2
3
MODELSCOPE_CACHE=F:/modelscope_cache uvx --from modelscope modelscope download \
--model BAAI/bge-large-zh-v1.5 \
--local_dir docker/embedding/bge-large-zh-v1.5

C 盘空间也顺便挪走:HF_HOME=F:/hf_cache

坑 4(虚惊一场):ModelScope 只有 pytorch_model.bin,没 model.safetensors

下载完成后我下意识去检查 TEI(text-embeddings-inference)的兼容性——它默认吃 safetensors,没有就报错。

结果实测发现:**TEI 的 candle backend 直接吃 pytorch_model.bin**,根本不用 safetensors,也不用装 torch。1.3G 的模型就这么直接用起来了。

⚠️ 这个坑是”我自己吓自己”——遇到报错前先看官方文档说什么,别被搜索结果里的”通用经验”误导。TEI 用 candle 后端时是支持 pytorch_model.bin 的。

第四章:Docker 编排——16G 内存下的容器战争

Docker 容器编排关系图
图 2:4 个核心容器(mysql/es/qdrant/TEI)+ 可选 kibana

坑 5:OOM 黑屏

docker compose up 拉起 5 个容器(含 kibana)时,整机黑屏 3 秒。重启后发现 ES 镜像 675MB 那层 extract 时直接把内存撑爆——16G 只剩 988MB,IDEA + Chrome 已经把可用内存吃干抹净。

1
2
3
4
# 关 IDEA
# 跳 kibana(非必须,只是 ES 可视化 UI)
docker compose -f docker/docker-compose.yaml up -d \
mysql elasticsearch qdrant embedding

只起 4 容器就稳了。kibana 是可选的 UI 工具,不影响主链路。

坑 6 + 7:容器名 + 端口双重冲突

启动 mysql 时又报:

1
2
container name "mysql" already in use
port 3306 is already allocated

本机装了 mysqld.exe(Windows MySQL 服务)占着 3306,还有个旧的 mysql 容器(别的项目残留)占着容器名。

两步解决

1
docker rm mysql   # 删旧容器
1
2
3
# docker/docker-compose.yaml
- "3306:3306"
+ "3307:3306"
1
2
3
4
5
6
7
8
9
# conf/app_config.yaml
db_meta:
host: 127.0.0.1
- port: 3306
+ port: 3307
db_dw:
host: 127.0.0.1
- port: 3306
+ port: 3307

容器内还是 3306(不用动),只把宿主端口错开

第五章:元数据知识库——aiomysql 的回旋镖

坑 8:aiomysql ping 签名 bug(异步适配器的副作用)

build_meta_knowledge.py(构建元数据知识库的脚本)时:

1
AsyncAdapt_aiomysql_connection.ping() missing 1 required positional argument: 'reconnect'

SQLAlchemy 开了 pool_pre_ping=True(每次取连接前 ping 一下),会调 connection.ping()。但 aiomysql 的 async 适配器要求 reconnect 参数(asyncmy 没这问题,是换驱动后的副作用)。

最干净的解决:**关掉 pool_pre_ping**。

1
2
3
4
5
# app/clients/mysql_client_manager.py
engine = create_async_engine(
url,
pool_pre_ping=False, # ← aiomysql 兼容性
)

💡 这个坑很隐蔽:换驱动的隐性代价只有运行时才暴露。**生产环境别只看”功能等价”,要看”配置参数签名等价”**。

坑 9:build 重跑主键冲突

第一次失败残留了部分数据,重跑就:

1
Duplicate entry 'dim_region.region_id' for key 'PRIMARY'

清表后重跑:

1
2
3
4
5
6
7
docker exec mysql mysql -u didilili -pdili123 meta -e \
"SET FOREIGN_KEY_CHECKS=0;
DELETE FROM column_info;
DELETE FROM column_metric;
DELETE FROM metric_info;
DELETE FROM table_info;
SET FOREIGN_KEY_CHECKS=1;"

小细节:mysql 命令在密码告警时会输出到 stderr,传统的 cmd 2>&1 | grep -v Warning 加上 && 链会因为 grep 在”空匹配时返回 1”而断链。改用 ; 分隔命令最稳。

最终:5 表 / 24 字段 / 2 指标 落库,元数据知识库就位。

第六章:后端启动——GBK 编码下的 emoji 血案

坑 10:fastapi dev 因 rich emoji 崩

按 README 跑:

1
uv run fastapi dev main.py

报错:

1
UnicodeEncodeError: 'gbk' codec can't encode character '\U0001f680'

fastapi-cli 用 rich 打印 banner,里面有 🚀 这样的 emoji。Windows 终端默认 GBK,根本编码不了 unicode。rich 的 win32 渲染器直接崩。

两条路:

  1. 绕开:用 uvicorn 直接起(没有 rich banner)
  2. 配环境:告诉 Python 用 UTF-8

我两条都做了:

1
2
PYTHONUTF8=1 PYTHONIOENCODING=utf-8 \
uv run uvicorn main:app --port 8000

后来(2026-07-18 复启)发现:只要带上 PYTHONUTF8=1fastapi dev 也能跑。这条经验也写进了 README。

后端起来后还会有一堆 jiebaSyntaxWarning: invalid escape sequence——无害,是 jieba 旧正则在 Python 3.14 下的告警,不影响功能。

第七章:模型选型心路——这是最长的一章

这是整篇博客最值得读的部分。前 10 个坑都是工程问题,坑 12 是 LLM 应用的核心架构问题

LLM 模型选型决策树
图 3:从 MiniMax-M2.7 到 M3 + extra_body 的四步选型路径

shopkeeper-agent 用 LangChain 的 PydanticOutputParser(一种 JSON 输出解析器)来约束 LLM 输出。解析器期望:

1
{"intent": "aggregation", "metric": "销售总额", ...}

推理模型(reasoner)默认会先输出一段思考过程

1
2
3
用户问的是华北地区的销售总额,我需要先找到对应区域字段,
然后聚合 sum(销售金额),按 region_name = '华北' 过滤...
{"intent": "aggregation", ...}

LangChain 的 JSON 解析器撞上 `` 标签就直接 OUTPUT_PARSING_FAILURE,整个 recall_value / recall_metric 节点失败。

查 MiniMax 官方文档说”M 系列不能关闭 think”——这个结论不准确

  • M2.x:确实不能关闭(即使传 thinking: {"type": "disabled"} 也会被忽略)
  • M3支持 thinking: {"type": "disabled"} 关掉 think 输出(内部思考能力保留,只是不返回 think 块)

解决(M3 + extra_body)

app/agent/llm.pyinit_chat_model(...) 加一行参数:

1
extra_body={"thinking": {"type": "disabled"}},

配合:

1
2
3
4
5
# conf/app_config.yaml
llm:
model_name: MiniMax-M3
api_key: ${oc.env:LLM_API_KEY}
base_url: https://api.minimaxi.com/v1

效果:M3 关 think 后输出纯 JSON,项目 LangChain 解析正常,端到端跑通(简单查询 + 复杂日期 JOIN 查询都过)。M3 内部推理能力保留(关的是输出,不是推理)。

模型选型的完整演进

阶段 模型 结果 备注
1 MiniMax-M2.7-highspeed ❌ JSON 解析炸 think 输出无法关闭
2 MiniMax-M3(默认) ❌ JSON 解析炸 同样 think 炸
3 qwen-plus / qwen-max ✅⚠️ 偶发错 简单查询过,复杂日期 JOIN 偶错
4 MiniMax-M3 + extra_body ✅ 完美 M3 内部强推理 + 输出纯 JSON

最终选了第 4 条路:用最强推理能力的模型 + 关掉其思考输出——既拿到 M3 的复杂推理能力,又拿到干净的 JSON 解析。

坑 13:Git Bash 自带 curl 坏了

端到端测试时碰到个小坑:

1
curl -X POST http://localhost:8000/api/query -d '{"query":"..."}'

curl 无任何输出(连错误都没有),后端日志也无请求记录。Git Bash 自带的 curl 坏了

1
C:/Program Files/Git/mingw64/bin/curl.exe: error while loading shared libraries

换成 Windows 自带 curl:

1
2
3
/c/Windows/System32/curl.exe -N -X POST http://127.0.0.1:8000/api/query \
-H "Content-Type: application/json" \
-d '{"query":"统计华北地区的销售总额"}'

返回:

1
{"type": "result", "data": [{"销售总额": 41099.5}]}

✅ 端到端跑通。

额外细节:用 127.0.0.1 不用 localhost(避免 IPv6 解析到 ::1,后端只听 IPv4)。

重启后端(换 LLM 后)

换完模型需要重启后端:

1
2
3
4
netstat -ano | grep ":8000 "          # 找 PID
taskkill //PID <pid> //F
cd shopkeeper-agent-main
PYTHONUTF8=1 PYTHONIOENCODING=utf-8 uv run uvicorn main:app --port 8000

第八章:前端补完 + 2026-07-18 复启

坑 11:node 14 太老 + pnpm 缺失(最终解法)

之前卡在 node 14 + pnpm 缺失。本次确认:

  • nvm.exe use 22.20.0(vite 6 + react 19 需 node 18+;本机 nvm 已有 18.20.8 / 20.18.0 / 22.20.0 / 24.9.0)
  • 不用装 pnpmfrontend/node_modules 早已存在,Git Bash 里 pnpm 又不在 PATH,直接用本地 vite:
1
cd frontend && npx --no-install vite

vite v6.4.2,端口 5173,/api 已代理到 8000(vite.config.tsVITE_DEV_PROXY_TARGET,默认 http://127.0.0.1:8000)。

本次完整启动顺序(5 步)

  1. 启 Docker Desktop(GUI,轮询 docker ps 直到通)
  2. docker compose -f docker/docker-compose.yaml up -d(5 容器;内存紧可 docker stop kibanarestart: unless-stopped 下不会自动重起)
  3. 后端 PYTHONUTF8=1 PYTHONIOENCODING=utf-8 uv run fastapi dev main.py(lifespan 自动连 4 个中间件;元数据在 docker volume 里,**无需重跑 build_meta_knowledge**)
  4. 前端 nvm use 22.20.0 && cd frontend && npx --no-install vite
  5. 浏览器开 http://localhost:5173 问数

排障时间线复盘

13 个坑的排障时间线
图 4:13 个坑按启动顺序排列的时间线

把 13 个坑按启动顺序拉成一条线,能看清几件事:

  • 前 1/3 是环境问题(Python、依赖、Embedding),全部是”装东西”阶段的
  • 中间 1/3 是中间件问题(Docker、Mysql、知识库构建),全部是”起服务”阶段的
  • 最后 1/3 是 LLM 和前端问题(模型选型、curl、node 版本),全部是”用起来”阶段的

每一阶段的失败都跟前一阶段强相关——这就是排障的真相:bug 不会单独出现,总是连环来

经验总结:13 个坑的根因分类

写完所有坑之后回头看,我把它们归到 5 个根因桶里:

1. Windows 适配(3 个)

  • asyncmy 编译失败(无 MSVC)
  • rich emoji 编码崩(GBK 终端)
  • Git Bash curl 共享库丢失

对策:项目一开始就走”纯 Python 优先 + UTF-8 强制”路线,能避开一大半 Windows 适配坑。

2. 国内网络(1 个)

  • hf-mirror 不稳 → ModelScope

对策:直接用 ModelScope 作为默认 HF 替代,别再赌 hf-mirror。

3. 环境冲突(2 个)

  • 端口 3306、容器名 mysql

对策:用 docker ps -a + netstat 先做端口/容器名扫描,再起新项目。

4. Python 3.14 太新(1 个)

  • asyncmy 无 wheel

对策:要么锁 Python 3.12,要么默认走纯 Python 驱动(aiomysql/PyMySQL)。

5. 内存紧张(1 个)

  • 16G 跑 5 容器 + 桌面程序 → OOM,跳 kibana 缓解

对策:docker compose 文件要分层——必选 vs 可选。kibana 这种纯可视化工具拆出来,内存不够时直接 skip。

6. LLM 架构(2 个)

  • 推理模型 think 输出与 JSON parser 不兼容
  • extra_body 关闭 think 是解药

对策:用 LangChain 的输出解析器时,默认假设模型会输出额外内容——优先选非推理模型,或者用 extra_body 关 think。

7. Node 生态(1 个)

  • node 14 EOL + pnpm 不在 PATH

对策:用 nvm 切 node 20+,项目根放个 .nvmrc 锁版本。

关键改动文件最终汇总

文件 改动 根因
ai-application/pyproject.toml version = "0.0.0" uv 扫祖先解析
shopkeeper-agent-main/pyproject.toml asyncmyaiomysql Windows 无 MSVC
app/clients/mysql_client_manager.py asyncmy→aiomysql + pool_pre_ping=False 驱动替换 + ping bug
conf/app_config.yaml LLM = MiniMax-M3 + mysql 端口 3307 模型切换 + 端口避让
docker/docker-compose.yaml mysql 端口 3307:3306 端口避让本机 mysql
app/agent/llm.py extra_body={"thinking":{"type":"disabled"}} 关 M3 think 输出
.env MiniMax API Key LLM 鉴权

下次启动(快速恢复)

同机器(数据/模型都在,3 步)

1
2
3
4
5
6
7
8
9
10
cd F:/bug图/zhioai/ai-application/ai-application/源码项目/shopkeeper-agent-main

# 1. 起 4 容器(数据在 docker volume,自动恢复)
docker compose -f docker/docker-compose.yaml up -d mysql elasticsearch qdrant embedding

# 2. 起后端
PYTHONUTF8=1 PYTHONIOENCODING=utf-8 uv run uvicorn main:app --port 8000

# 3. 起前端(另开一个终端)
cd frontend && npx --no-install vite

浏览器开 http://localhost:5173 即可问数。

关 Docker(数据安全)

  • 直接关 Docker Desktop 即可,数据安全:mysql/es/qdrant 数据在 docker volume(mysql_data/es_data/qdrant_data),关 Docker 只是停容器,不删 volume,下次 docker compose up 数据自动恢复
  • embedding 模型在本地 docker/embedding/bge-large-zh-v1.5/(1.3G,gitignore 不传 GitHub),只要不删这个目录,下次不用重下
  • ⚠️ 别用「Reset Docker to factory defaults」或手动删 volume,那会清空 mysql/es/qdrant 数据,要重跑 build_meta_knowledge

结语

写到这里回头看,13 个坑单独看都不大,但叠加起来就是两天的工作量。这也正是”AI 应用项目”的真实写照——核心代码可能只占 20%,剩下 80% 全在和”环境、依赖、网络、模型版本”做斗争。

如果这篇博客能帮你省下哪怕一个小时的排障时间,那这 4000 字就值了。

欢迎在评论区分享你的踩坑故事——我相信每个跑过本地 LLM 项目的人都有一个”坑 12”故事