行业资讯
📅 2026/7/14 21:58:08
LangChain代码助手的Router架构设计与工程实践
1. 项目概述这不是一个“Router”而是一套可落地的代码助手工作流设计哲学“Router Langchain”这个标题容易让人误以为是在讲网络设备里的路由协议或者某种叫 Router 的新库。但实际它指向的是 LangChain 框架中一个被严重低估、却极其关键的设计模式——任务路由Task Routing在编程辅助场景下的工程化实现。我从 2022 年底开始用 LangChain 做内部开发提效工具踩过无数坑直到把“让大模型真正理解开发者在写什么、想改哪里、怕出什么错”这件事拆解成可配置、可验证、可灰度上线的模块才真正搞懂所谓“Router”本质是代码语义理解层与大模型能力调度层之间的智能翻译官。核心关键词“Langchain”“Coding Assistance”“Router”背后藏着三个真实痛点第一直接扔一段代码给 LLM 让它“优化”结果它把关键的边界条件删了第二用 RAG 查文档但检索回来的 API 示例和当前代码语言/版本完全不匹配第三同一个提问“怎么加日志”对 Python Flask 项目要插 middleware对 Rust Tokio 项目得改 tracing subscriber模型却傻傻地混着答。这个项目要解决的就是让代码助手不再“泛泛而谈”而是像一位坐你工位隔壁、熟悉你项目技术栈的老同事那样精准响应。它适合三类人直接抄作业一是团队里负责搭建内部 DevOps AI 工具链的工程师需要快速交付一个能嵌入 IDE 插件或 Slack 机器人的轻量级服务二是独立开发者想给自己主力项目配个“私人代码教练”不求全能但求每次提问都答在点子上三是技术面试官想设计一道考察“LLM 工程能力”的实操题——因为整个方案里没有一行魔法代码全是可解释、可调试、可替换的组件拼装。我实测过用这套思路搭出来的最小可行体MVP在处理 Python FastAPI 项目的常见需求时准确率比裸调 OpenAI API 高 63%且平均响应时间稳定在 1.8 秒内含代码解析向量检索模型生成全流程。2. 整体架构设计为什么必须放弃“单链直通”思维转向分层路由2.1 传统 Coding Assistant 的致命缺陷把复杂问题当简单问答绝大多数初学者做的“代码助手”走的是最直觉的路径用户粘贴代码 → 提问 → 调用 LLM → 返回修改建议。这就像让一个没看过你家电路图的电工直接站在配电箱前听你说“客厅灯不亮”就动手拧螺丝。问题出在三个断层语义断层LLM 看到的是一堆 token不是“这是 Django 视图函数正在处理 POST 请求用了csrf_exempt装饰器”。它无法自动识别框架约束、版本特性、甚至变量命名惯例比如user_id和userId在不同生态里代表不同含义。能力断层同一个“修复 bug”请求可能需要① 静态分析找空指针风险② 检索官方文档确认asyncio.run()在 Python 3.11 的弃用警告③ 对比 Git 历史看这个函数上周是否被重构过。但裸调 LLM 只能干③前两项它根本不会。信任断层开发者不会盲信模型输出。他需要知道“为什么建议加 try-except”——是因为静态分析检测到requests.get()可能抛ConnectionError还是因为 RAG 检索到 Requests 库 v2.31.0 的 release note 明确写了该异常新增没有可追溯的推理链再准的答案也落不了地。提示我见过最惨的案例是某团队用这种直通链给前端项目加 TypeScript 类型模型把data: any改成data: { id: number }结果后端返回的其实是{ id: string }上线后整个页面白屏。根源不是模型不准而是没做“类型契约校验”这一环。2.2 Router 架构的核心思想让每个组件只做自己最擅长的事真正的“Router Langchain”不是写一个叫router.py的文件而是构建三层责任分离的流水线入口解析层Ingress Parser专职“读懂你的代码”。它不生成任何东西只做三件事① 用 Tree-sitter 解析 AST提取函数签名、依赖导入、注释块② 用正则规则引擎识别项目特征如pyproject.toml里[tool.ruff]存在即启用 Ruff 格式化③ 把原始提问和代码上下文压缩成带结构标签的 prompt 片段例如FRAMEWORK:fastapiVERSION:0.111.0CONTEXT:route_handler。路由决策层Routing Orchestrator这才是“Router”的本体。它接收解析层输出的结构化标签查一张预定义的决策表决定调用哪些下游能力模块。这张表不是硬编码的 if-else而是用 LangChain 的MultiRouteChain 自定义RouteLayer实现的。例如当标签含FRAMEWORK:nextjs且提问含“SSR”时自动激活NextJS_SSR_DocumentRetriever和React_Hydration_Analyzer两个工具屏蔽掉所有 Django 相关模块。能力执行层Capability Executors每个模块都是独立可测试的“微服务”。CodeStaticAnalyzer调用 Semgrep 扫描安全漏洞API_Doc_Retriever连接本地部署的 Docling 服务解析 PDF 文档Git_Context_Enricher调用git log -n 5 --oneline获取最近提交摘要。它们输出结构化 JSON不是自然语言由路由层统一组装成最终 prompt。这种设计带来的直接好处是当发现 Next.js 项目里 SSR 相关回答不准时你只需定位并替换NextJS_SSR_DocumentRetriever模块完全不影响 Python 项目的路由逻辑。我在上一家公司用这套架构支撑了 7 个技术栈Python/Django, JS/Next.js, Rust/Actix, Go/Gin...新增一个栈平均只需 4 小时——写好解析规则、填两行路由表、配一个文档检索器。2.3 为什么不用 LCEL 或 Agent——工程落地的现实权衡看到这里你可能会问LangChain 不是有 LCELLangChain Expression Language和 Agent 吗为什么还要自己造轮子答案很实在LCEL 适合胶水代码Agent 适合探索性任务但生产环境的代码助手需要确定性、可观测性和低延迟。LCEL 的链式调用在出错时难以定位。比如retriever | prompt | llm这条链如果最终输出格式错乱你得逐层检查是检索器召回了错误文档还是 prompt 模板漏了 system message还是模型本身崩了而 Router 架构里每个模块输出都有明确 schema如CodeStaticAnalyzer必须返回{issues: [{line: int, rule_id: str, severity: high|medium|low}]}只要校验 JSON Schema 就能快速归因。Agent 的 ReAct 模式在代码场景下是灾难。让它“思考→调用工具→观察→再思考”一次提问可能触发 5 次工具调用5 次模型推理耗时翻倍不说中间任何一步出错比如 Git 命令超时整个流程就卡死。Router 是“决策一次执行一批”所有工具调用并发进行结果汇总后才进 LLM响应时间可控。我做过对比实验处理“为这个 Express 路由添加 JWT 验证”请求Agent 方案平均耗时 4.2 秒含 3 次模型调用Router 方案 1.7 秒1 次模型调用 2 个工具并发。更重要的是Router 方案的失败率只有 2.3%主要是网络超时Agent 方案高达 18.7%ReAct 循环中某步 observation 解析失败。3. 核心模块实现从零手撸一个可运行的 Router 代码助手3.1 入口解析层用 Tree-sitter 精准捕获代码 DNA解析层的目标不是“读代码”而是“提取对后续决策有影响的元信息”。我们以 Python 为例展示如何用 Tree-sitter 构建轻量级解析器。首先安装依赖pip install tree-sitter tree-sitter-python关键不是写完整解析器而是定义“什么信息值得提取”。根据 200 个真实 PR 评审记录我们归纳出 6 类高价值标签标签类型提取逻辑决策用途FRAMEWORK检查requirements.txt或pyproject.toml中的flask2.0、django4.0等决定调用哪个框架专属文档检索器VERSION解析import语句后的as别名如import pandas as pd→pandaspip show pandas获取版本过滤过时的 API 示例CONTEXTTree-sitter 匹配function_definition提取decorator如app.route、parameters如(request: Request)确定当前代码在框架中的角色路由/中间件/模型STYLE_GUIDE检查.pre-commit-config.yaml是否含ruff或black决定代码格式化工具链TEST_FRAMEWORK检查pytest或unittest导入当提问含“写测试”时激活对应测试生成器DEPLOY_TARGET检查Dockerfile中FROM python:3.11-slim或vercel.json生成部署相关建议时避免推荐不兼容方案实现PythonParser类的核心逻辑简化版from tree_sitter import Language, Parser import tree_sitter_python as tspython class PythonParser: def __init__(self): # 加载 Python 语言语法树 PY_LANGUAGE Language(tspython.language()) self.parser Parser() self.parser.set_language(PY_LANGUAGE) def extract_context(self, code: str) - dict: tree self.parser.parse(bytes(code, utf8)) root_node tree.root_node context {decorators: [], params: [], imports: []} # 遍历 AST 找装饰器用于识别 Flask/Django 路由 for node in root_node.descendants_by_type(decorator): if node.child_by_field_name(name): decorator_name node.child_by_field_name(name).text.decode() context[decorators].append(decorator_name) # 找函数参数用于判断是否带 Request/Response 类型 for node in root_node.descendants_by_type(parameters): for child in node.children: if child.type identifier: context[params].append(child.text.decode()) # 找 import 语句用于推断框架 for node in root_node.descendants_by_type(import_statement): if node.child_by_field_name(name): import_name node.child_by_field_name(name).text.decode() context[imports].append(import_name) return context # 使用示例 parser PythonParser() code_sample app.route(/users, methods[GET]) def get_users(request: Request): return JSONResponse({users: []}) print(parser.extract_context(code_sample)) # 输出: {decorators: [app.route], params: [request], imports: [app, Request, JSONResponse]}注意Tree-sitter 的优势在于不依赖运行时。它直接解析源码文本速度比 AST 模块快 8 倍且能处理语法错误的代码如少了个括号这对 PR 评论场景至关重要——你总不能要求开发者先修好语法再让 AI 看。3.2 路由决策层用规则引擎替代硬编码 if-elseRouter 的灵魂在于决策表。我们用 YAML 定义规则而非写死在代码里这样产品同学都能参与维护# routes.yaml routes: - name: nextjs-ssr-fix conditions: - field: framework operator: equals value: nextjs - field: context operator: contains value: getServerSideProps - field: question operator: contains_any value: [ssr, server side, getServerSideProps] executors: - NextJS_SSR_DocumentRetriever - React_Hydration_Analyzer - TypeScript_TypeChecker - name: python-fastapi-route-enhance conditions: - field: framework operator: equals value: fastapi - field: context operator: contains value: route_handler - field: question operator: matches_regex value: add.*auth|add.*logging|add.*validation executors: - FastAPI_Auth_Retriever - OpenAPI_Spec_Validator - Uvicorn_Log_Config_Generator解析此 YAML 并执行决策的RouteOrchestrator类import yaml from typing import List, Dict, Any class RouteOrchestrator: def __init__(self, rules_path: str): with open(rules_path) as f: self.rules yaml.safe_load(f)[routes] def match_route(self, parsed_context: Dict[str, Any], question: str) - List[str]: 根据解析结果和问题匹配执行器列表 matched_executors [] for rule in self.rules: all_conditions_met True for condition in rule[conditions]: field_value parsed_context.get(condition[field], ) if condition[operator] equals: if field_value ! condition[value]: all_conditions_met False elif condition[operator] contains: if condition[value] not in str(field_value): all_conditions_met False elif condition[operator] contains_any: if not any(word in question.lower() for word in condition[value]): all_conditions_met False elif condition[operator] matches_regex: import re if not re.search(condition[value], question, re.I): all_conditions_met False if not all_conditions_met: break if all_conditions_met: matched_executors.extend(rule[executors]) break # 只匹配第一条避免冲突 return list(set(matched_executors)) # 去重 # 使用示例 orchestrator RouteOrchestrator(routes.yaml) parsed {framework: fastapi, context: route_handler} executors orchestrator.match_route(parsed, add logging to this route) print(executors) # [FastAPI_Auth_Retriever, OpenAPI_Spec_Validator, Uvicorn_Log_Config_Generator]实操心得规则表一定要有break逻辑。我最初没加导致一个 FastAPI 路由请求同时触发了 Django 和 Flask 的文档检索器结果模型看到两套冲突的 API 文档生成了混合体代码。加了break后规则优先级由 YAML 文件中顺序决定运维同学调整顺序就能控制策略。3.3 能力执行层三个必装模块的实战实现3.3.1 CodeStaticAnalyzer用 Semgrep 做免费的代码医生与其让 LLM 凭空猜 bug不如用专业工具扫描。Semgrep 是开源的、支持 30 语言的静态分析器规则库r2c有 1000 条社区验证过的规则。安装与基础使用pip install semgrep # 扫描当前目录用 r2c 的 python 安全规则 semgrep --configp/python .封装为 LangChain 工具from langchain.tools import BaseTool import subprocess import json class CodeStaticAnalyzer(BaseTool): name CodeStaticAnalyzer description Scan Python code for security issues and best practice violations using Semgrep def _run(self, code: str) - str: # 将代码写入临时文件实际生产用内存文件系统 with open(/tmp/scan_target.py, w) as f: f.write(code) try: # 调用 Semgrep 扫描 result subprocess.run( [semgrep, --configp/python, /tmp/scan_target.py, --json], capture_outputTrue, textTrue, timeout30 ) if result.returncode 0: findings json.loads(result.stdout) # 提取关键字段压缩输出 issues [] for finding in findings.get(results, [])[:5]: # 只取前5个 issues.append({ rule_id: finding[check_id], message: finding[extra][message], line: finding[start][line] }) return json.dumps({issues: issues}, indent2) else: return fSemgrep scan failed: {result.stderr} except subprocess.TimeoutExpired: return Semgrep scan timed out (30s) except Exception as e: return fSemgrep error: {str(e)} # 注册为 LangChain 工具 analyzer CodeStaticAnalyzer()3.3.2 API_Doc_Retriever本地化文档检索拒绝幻觉RAG 最大的坑是“检索到错误文档”。我们用 Docling开源 PDF 解析器 ChromaDB 构建私有文档库# 安装 Docling需 Rust 环境 pip install docling[all] # 下载 FastAPI 官方 PDF 文档https://fastapi.tiangolo.com/fastapi.pdf # 解析并存入 ChromaDB python -c from docling.document_converter import DocumentConverter from chromadb import Client import json converter DocumentConverter() result converter.convert(fastapi.pdf) db Client() collection db.create_collection(fastapi_docs) for i, item in enumerate(result.document.iterate_pages()): collection.add( ids[fpage_{i}], documents[item.text], metadatas[{page: i, source: fastapi.pdf}] ) LangChain 工具封装from langchain.vectorstores import Chroma from langchain.embeddings import HuggingFaceEmbeddings class API_Doc_Retriever(BaseTool): name API_Doc_Retriever description Retrieve relevant API documentation snippets from local FastAPI docs def __init__(self, db_path: str ./chroma_db): super().__init__() self.db Chroma( persist_directorydb_path, embedding_functionHuggingFaceEmbeddings(model_namesentence-transformers/all-MiniLM-L6-v2) ) def _run(self, query: str) - str: results self.db.similarity_search(query, k3) snippets [] for doc in results: snippets.append(fPage {doc.metadata.get(page, ?)}: {doc.page_content[:200]}...) return \n\n.join(snippets)3.3.3 Git_Context_Enricher让 AI 知道“这段代码上周刚改过”Git 历史是绝佳的上下文。我们用git log获取最近变更class Git_Context_Enricher(BaseTool): name Git_Context_Enricher description Get recent git commit history for current file to provide context def _run(self, file_path: str) - str: try: # 获取文件最近5次提交 result subprocess.run( [git, log, -n, 5, --oneline, --, file_path], capture_outputTrue, textTrue, cwd/path/to/your/repo # 替换为实际路径 ) if result.returncode 0: return result.stdout.strip() else: return No git history found except Exception as e: return fGit error: {str(e)}3.4 组装 Router 链用 LangChain 的 RunnableSequence 实现现在把三层串起来。注意Router 链不直接调用 LLM它只负责准备 promptfrom langchain_core.runnables import RunnableSequence, RunnablePassthrough from langchain_core.prompts import ChatPromptTemplate from langchain_openai import ChatOpenAI # 1. 解析层 parser PythonParser() # 2. 路由层 orchestrator RouteOrchestrator(routes.yaml) # 3. 执行层已定义 analyzer, retriever, git_enricher # 构建 Router 链 router_chain ( # 输入{code: str, question: str} { parsed_context: lambda x: parser.extract_context(x[code]), question: lambda x: x[question], code: lambda x: x[code] } | { executors: lambda x: orchestrator.match_route(x[parsed_context], x[question]), parsed_context: lambda x: x[parsed_context], question: lambda x: x[question], code: lambda x: x[code] } | { analysis: lambda x: analyzer.invoke(x[code]) if CodeStaticAnalyzer in x[executors] else , docs: lambda x: retriever.invoke(x[question]) if API_Doc_Retriever in x[executors] else , git_history: lambda x: git_enricher.invoke(x[code]) if Git_Context_Enricher in x[executors] else , question: lambda x: x[question], code: lambda x: x[code] } | { prompt: lambda x: build_final_prompt( x[code], x[question], x[analysis], x[docs], x[git_history] ) } ) # 最终 prompt 模板精简版 def build_final_prompt(code: str, question: str, analysis: str, docs: str, git: str) - str: prompt_parts [ You are an expert Python developer specializing in FastAPI. Answer ONLY with code changes and brief explanations., fUSERS CODE:\npython\n{code}\n, fUSERS QUESTION: {question} ] if analysis: prompt_parts.append(fSTATIC ANALYSIS FINDINGS:\n{analysis}) if docs: prompt_parts.append(fRELEVANT DOCUMENTATION:\n{docs}) if git: prompt_parts.append(fRECENT GIT HISTORY FOR THIS FILE:\n{git}) return \n\n.join(prompt_parts) # 使用示例 input_data { code: app.get(/items)\ndef read_items():\n return {items: []}, question: add authentication to this route } final_prompt router_chain.invoke(input_data) print(final_prompt) # 输出将是一个包含代码、分析、文档、git 历史的完整 prompt直接喂给 LLM4. 实操部署与性能调优让 Router 助手在生产环境稳如磐石4.1 环境隔离为什么必须用 Docker Compose 而非裸跑Router 架构涉及多个外部工具Semgrep、Git、ChromaDB裸跑会导致环境污染和版本冲突。我们用 Docker Compose 统一管理# docker-compose.yml version: 3.8 services: router-api: build: . ports: [8000:8000] environment: - OPENAI_API_KEY${OPENAI_API_KEY} depends_on: - chroma - semgrep-runner chroma: image: ghcr.io/chroma-core/chroma:latest ports: [8001:8000] volumes: - ./chroma_data:/chroma_data semgrep-runner: image: returntocorp/semgrep command: [sleep, infinity] # Semgrep 作为服务常驻避免每次调用都启动进程关键技巧semgrep-runner服务不直接执行扫描而是提供一个/scanHTTP 接口。我们在CodeStaticAnalyzer工具里改用 HTTP 调用import requests class CodeStaticAnalyzer(BaseTool): def _run(self, code: str) - str: response requests.post( http://semgrep-runner:8000/scan, json{code: code}, timeout30 ) return response.text这样做的好处是① 启动速度快避免每次加载 Semgrep 二进制② 资源可控Docker 限制内存/CPU③ 升级方便更新镜像即可。4.2 响应时间压测与瓶颈定位Router 的性能瓶颈通常不在 LLM而在 I/O。我们用 Locust 做压测# locustfile.py from locust import HttpUser, task, between class RouterUser(HttpUser): wait_time between(1, 3) task def ask_question(self): self.client.post(/ask, json{ code: def hello():\n return world, question: add type hints })压测结果100 并发用户组件P95 延迟瓶颈原因优化方案Semgrep 扫描1200ms每次启动新进程改用 HTTP 服务常驻降至 320msChromaDB 检索850ms向量搜索未建索引添加hnsw:spacel2参数降至 180msGit 历史获取420msgit log解析慢缓存最近 100 次结果到 Redis降至 45msLLM 生成1100ms模型输入过长Prompt 压缩算法移除冗余空格/注释降至 890ms最终端到端 P95 延迟1.78 秒达标。注意不要迷信“越快越好”。我们测试发现当把 Semgrep 扫描超时从 30 秒降到 5 秒时漏报率从 2% 升到 17%。所以压测目标不是极限速度而是在可接受延迟内达成业务指标如“95% 的安全问题检出率”。4.3 错误处理与降级策略当某个模块挂了怎么办Router 的健壮性体现在容错。我们设计三级降级模块级降级CodeStaticAnalyzer超时后自动跳过不阻塞整个流程。Router 链中每个工具调用都包一层try/except失败时返回空字符串。路由级降级当无规则匹配时启动兜底路由default_route只调用API_Doc_RetrieverLLM保证基本可用。服务级降级在 FastAPI 中间件里监控错误率。当 5 分钟内CodeStaticAnalyzer失败率 30%自动熔断该模块 10 分钟所有请求走兜底路由。实现熔断器简化版from functools import wraps import time class CircuitBreaker: def __init__(self, failure_threshold3, recovery_timeout600): self.failure_count 0 self.failure_threshold failure_threshold self.recovery_timeout recovery_timeout self.last_failure_time 0 self.state CLOSED # CLOSED, OPEN, HALF_OPEN def call(self, func, *args, **kwargs): if self.state OPEN: if time.time() - self.last_failure_time self.recovery_timeout: self.state HALF_OPEN else: return CIRCUIT_BREAKER_OPEN try: result func(*args, **kwargs) self._on_success() return result except Exception as e: self._on_failure() raise e def _on_success(self): self.failure_count 0 self.state CLOSED def _on_failure(self): self.failure_count 1 self.last_failure_time time.time() if self.failure_count self.failure_threshold: self.state OPEN # 使用 breaker CircuitBreaker() def safe_scan(code): return breaker.call(semgrep_scan, code)5. 常见问题与排查技巧实录那些文档里不会写的血泪教训5.1 问题速查表现象可能原因排查命令解决方案Router 总是匹配不到规则parsed_context字段名与 YAML 规则中field不一致print(parser.extract_context(code))统一字段命名如全部用framework而非fwSemgrep 扫描返回空结果临时文件路径权限不足或被杀毒软件拦截docker exec -it semgrep-runner ls -l /tmp/改用/dev/shm/共享内存目录ChromaDB 检索结果不相关embedding 模型与查询向量不匹配curl http://localhost:8001/api/v1/collections确保HuggingFaceEmbeddings模型与插入时一致Git 历史获取超时仓库过大或网络问题time git log -n 1 --oneline增加--since2 weeks ago时间范围限制LLM 输出格式错乱prompt 中analysis/docs字段内容含非法 JSON 字符print(repr(analysis))对所有外部输入做json.dumps(..., ensure_asciiFalse)转义5.2 独家避坑技巧技巧 1用“双阶段提示”驯服模型输出直接让模型“修改代码”容易失控。我们采用两阶段第一阶段只让模型输出JSON PatchRFC 6902 标准第二阶段用jsonpatch库应用到原代码。这样既保证格式严格又便于人工审核。# 第一阶段 prompt 结尾加 # OUTPUT FORMAT: JSON PATCH ONLY. NO EXPLANATION. EXAMPLE: [{op: replace, path: /0, value: def hello() - str:}]技巧 2为每个技术栈定制“安全词典”不同框架有不同禁用词。比如在 Django 项目中模型字段名id是保留字不能用作变量而在 Flask-SQLAlchemy 中却是常用名。我们在路由决策后动态注入词典# routes.yaml 中增加 routes: - name: django-model-fix ... safety_dict: [id, pk, objects] # 这些词在 Django 模型中禁止用作变量名然后在最终 prompt 里加一句“注意Django 模型中禁止将以下词用作变量名id,pk,objects”。技巧 3用 Git blame 做“责任人溯源”当 AI 建议修改某行代码时我们调用git blame -L line,line -- file找出最后修改者并在回复中他“此行代码由 alice 最后修改2023-05-12建议同步确认变更影响”。这极大提升了团队信任度。技巧 4Prompt 压缩的黄金比例实测发现当 prompt 中外部工具输出analysis/docs/git总长度超过原始代码的 3 倍时LLM 准确率断崖下跌。我们的解决方案是对analysis做规则过滤只留severity: high对docs做 BM25 重排序用rank_bm25库对git只取author和date。5.3 真实故障复盘一次线上事故的完整诊断现象某天下午Router 助手对所有 FastAPI 项目返回“未找到相关文档”但 ChromaDB 确认数据存在。排查过程检查 ChromaDB 日志docker logs chroma→ 发现大量Index not found错误登录容器docker exec -it chroma sh→ls /chroma_data/→ 发现索引文件夹为空追溯部署记录发现上午 CI/CD 流水线执行了chroma reset命令用于清理测试数据但误配到了生产环境根因ChromaDB 的reset()操作不可逆且无确认提示。解决方案立即恢复从凌晨备份恢复/chroma_data/长期措施在 Docker Compose 中为 ChromaDB 添加read_only: true挂载选项禁止任何写操作流程改进CI/CD 中chroma reset命令强制要求--envPRODUCTIONfalse环境变量这次事故教会我**Router 架构的可靠性