news 2026/10/22 17:48:00

Chroma实战:用轻量级向量数据库快速构建本地化RAG系统

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Chroma实战:用轻量级向量数据库快速构建本地化RAG系统

做 RAG 项目这几年,我最大的感受是:真正决定系统上限的,往往不是大模型本身,而是你背后那套知识检索链路。而这条链路的起点,就是向量数据库。在尝试过好几款产品之后,我长期留在项目里的,是开源的 AI 原生向量数据库 Chroma——它最打动我的点就一个字:轻。轻到不需要单独部署服务,一条命令装完,Python 进程里直接跑,数据落在本地目录,对中小团队和快速验证场景极其友好。

这篇文章写给两类人:一类是刚接触检索增强生成(RAG)的新手,想知道向量数据库到底解决什么问题、Chroma 怎么上手;另一类是在生产环境里被其他向量库的运维成本折腾得够呛的老手,想找个更轻量、更可控的本地化方案。文章会从 RAG 的底层逻辑讲起,然后逐步展开,包括环境准备、核心概念、完整的搭建与查询代码、生产环境配置、常见问题排查,以及我在真实项目中踩过的坑。只要你按着实操步骤走,一个能跑通的最小 RAG 基座,大概半小时就能落地。

1. 为什么 RAG 需要“原生”向量数据库

1.1 RAG 到底解决什么问题

大模型的知识来自训练数据,训练数据有截止时间,而且它完全没有你公司内部文档、产品手册、售后记录里那些私有知识的印象。你问它“我这个私有系统的某个模块怎么配置”,它要么一本正经地胡编,要么直接说不知道。

RAG 的思路很朴素:在模型开口之前,先从你准备好的知识库里检索出相关资料片段,把片段拼进提示词里,再让模型基于这些片段作答。相当于你给模型配了一个随时能查的资料文件柜,它每次回答前先翻一会儿档案,再开口说话。

一个标准的 RAG 流程由五个环节串联而成:

  1. 加载文档:把 PDF、Markdown、纯文本、网页等各类资料读入程序。
  2. 切分与清洗:长文档切成适当大小的块,去掉目录、页眉页脚等噪音。
  3. 向量化:用嵌入模型(Embedding Model)把每个文本块转成一串高维数字向量。
  4. 存储与索引:向量连同原文、元数据一起写入向量数据库,并建立高效索引。
  5. 检索与生成:查询时把问题也向量化,用相似度从库里召回最相关的块,塞进 Prompt,交给大模型。

前两步决定“喂进去的原料干不干净”,第三步决定“向量表达准不准”,第四、第五步之间,就是向量数据库的主场。

1.2 普通数据库为什么顶替不了向量库

有些刚起步的人会问:我不就是存个向量吗,往 MySQL 里开个字段塞进去不就行了?

你确实能硬塞进去,但问题出在“检索”这一步。向量相似度查询需要计算目标向量和库内大量向量之间的距离。普通数据库没有为这种高维距离计算设计索引结构,数据量几千条时暴力扫描还能忍,到几万、几十万条时,每次查询都要全表过一遍,延迟直接失控。

Chroma 这类原生向量数据库,核心价值在于内部实现了近邻索引结构。它会提前对向量做空间切分和组织,查询时只需要访问一小部分候选区域,用极小精度损耗换来几个数量级的速度提升。这个特性,恰恰是 RAG 场景最需要的。

1.3 主流开源向量数据库对比

选型的时候,我重点看了几款开源产品和零门槛方案,这里直接放一份对比表,方便你根据自己的场景来对照。

方案部署模式上手难度定位适用场景
Chroma进程内嵌入式,可持久化到本地低轻量级 AI 原生向量库中小项目、本地私有化、快速验证
某重型向量库 A独立分布式服务高大规模高并发向量检索百万级向量以上的在线服务
某重型向量库 B独立服务 + 多租户高云原生向量检索平台企业级多业务线隔离
某全文检索引擎扩展独立服务中全文检索 + 向量检索混合已有搜索架构,需要兼顾关键词

我选择 Chroma 不是因为它比重型产品功能更强,而是因为它解决了 80% 团队 80% 的初期需求,却只需要 5% 的维护成本。可以在开发阶段用内存模式飞快调试,生产阶段切到持久化模式,数据文件备份迁移都极其方便。等真正到了百万级向量、高并发在线服务的规模,再考虑是否切换底座也不迟。

1.4 Chroma 的定位与边界

Chroma 给自己的定位是“AI 原生向量数据库”,意思是它从设计之初就不是为了通用检索而生的,而是围绕 RAG 和 AI 应用的工作方式来做产品。

它有几个很实际的特性:

  • 内置默认嵌入函数,最小场景可以不额外接任何 Embedding 服务。
  • 提供持久化 API,数据默认落到本地目录,彻底解决内存模式丢数据的问题。
  • 写入、查询接口非常直觉化,和 Python 字典的操作体验接近。
  • 元数据过滤、集合管理这些能力,支撑生产环境的基础需求足够。

当然它的边界也很清楚:不是分布式数据库,不适合海量数据和高并发直查场景;全文检索能力不是它的强项;在超大规模需要弹性伸缩的时候,重型方案会更合适。但这不妨碍它在“一个团队快速搭起高质量 RAG”这件事上,成为我目前最推荐的选择。

2. 正式动手前必须吃透的几个概念

2.1 安装零障碍,但版本要注意

安装只需要一条命令:

pip install chromadb

Chroma 对 Python 版本有要求,当前主版本建议在 Python 3.9+ 环境下安装。我吃过一次亏,在某个仍用 3.8 的环境里直接装最新版,结果依赖解析就失败,后来锁定旧版本才装上。所以干净环境里安装前,先顺手确认一下版本:

python --version

如果你平时用虚拟环境管理项目依赖,这里强烈建议先进虚拟环境再安装,避免污染全局环境。装完之后验证一下:

import chromadb print(chromadb.__version__)

能正常打印版本号,说明环境已经就绪。

2.2 两种运行模式:内存与持久化

这是最容易忽略、也最影响使用体验的一个点。Chroma 有两种客户端创建方式:

# 内存模式:数据只存在进程内存里,程序退出数据就没了 client = chromadb.Client() # 持久化模式:数据写入本地磁盘目录 client = chromadb.PersistentClient(path="./chroma_store")

开发调试阶段用内存模式非常舒服,不用关心清理残留数据,因为进程一结束,一切归零。但生产环境必须用持久化模式,这个path目录里会保存 SQLite 元数据文件和向量索引文件,备份和迁移直接处理这个目录即可。

我见过有人把内存模式忘了切,写了几千条数据,服务一重启全没了,欲哭无泪。记住:内存模式只能用于开发和测试,生产一律持久化。

2.3 四个核心对象:Client、Collection、Document、Metadata

  • Client:入口对象,负责连接内存或磁盘数据目录,管理底下的集合。
  • Collection:可以把它理解成传统数据库里的“表”。每个集合有名字、可选的嵌入函数,以及一批向量文档。我建议一个集合承载一类业务数据,不要所有内容混在一个桶里。
  • Document:写入的最小文本单位,可以是一个句子、一个段落,或者一个语义块。
  • Metadata:挂在文档上的标签,格式是字典,比如来源文件名、章节号、写入时间、业务线。查询时可以通过where参数按元数据过滤,这是提升检索精度的重要手段。

2.4 距离函数的选择逻辑

创建集合时,可以通过元数据指定hnsw:space,可选值有三个。

距离函数参数值适用场景
余弦相似度cosine绝大多数文本检索场景,重点关注语义方向而非长度
欧氏距离l2图片、特征向量等对模长有要求的场景
内积ip高维稀疏向量或特定算法场景

文本类 RAG 项目,我基本都选 cosine。原因很简单:主流嵌入模型大多以余弦相似度为优化目标,选择一致的度量方式,查询结果和距离值都更容易解释。

3. 半小时搭一个能用 RAG 基座

3.1 嵌入模型:默认还是本地化

Chroma 自带一个默认嵌入函数,底层使用某个轻量级句向量模型,适合在有网环境里做快速验证。但如果你要本地化部署,或者追求更好的中英文效果,建议直接换成自己的嵌入模型。

我当前项目里用的是本地化的bge-m3,多语言能力稳,效果扎实。配置方法也很直接:

from chromadb.utils import embedding_functions ef = embedding_functions.SentenceTransformerEmbeddingFunction( model_name="BAAI/bge-m3", device="cpu" # 生产环境建议改成 "cuda" )

如果你有 GPU 资源,建议把device设为cuda,全量嵌入文档时能省下大把时间。CPU 推理不是不能用,只是当文档量大且切块细的时候,几百个块跑完,耗时和耐心都是考验。

3.2 文档加载与切分:不能按字节一刀切

先把文档读进来,这里以 Markdown 文件为例:

from pathlib import Path text = Path("./product_guide.md").read_text(encoding="utf-8")

切分我强烈不建议按固定字符数硬切。固定长度看起来简单,但会把语义完整的章节拦腰截断,导致检索出来的片段只有半句话,大模型看得莫名其妙。我在真实项目里是用“结构切分 + 片段重叠”的方式:

  1. 优先按换行、段落、标题等结构边界切。
  2. 切出的块太短,就向上合并,保证每块有完整的语义。
  3. 相邻块之间保留几十个字的重叠,防止关键句刚好落在边界上被割裂。

这里给一个快速验证用的切分函数:

def split_document(text, chunk_size=500, overlap=50): chunks = [] start = 0 while start < len(text): end = start + chunk_size if end < len(text): # 尽量在换行符位置切断,而不是生硬截断 cut = text.rfind("\n", start, end) if cut != -1 and cut - start > chunk_size * 0.5: end = cut chunks.append(text[start:end]) start = end - overlap return chunks

需要注意,这里按字符遍历没问题,但千万别用字节去切中文文本,否则很容易把一个 UTF-8 汉字从中间劈开,出来一堆乱码。切分字数也不是越大越好:块太大,塞进 Prompt 会浪费上下文;块太小,单块信息不足,检索准确率反而下降。我常用的经验区间是 300 到 800 字,具体值要根据你的文档类型微调。

3.3 创建集合并写入数据

有了嵌入函数和切分好的文本块,接下来就可以建集合、写数据了。

collection = client.get_or_create_collection( name="docs_v1", embedding_function=ef, metadata={"hnsw:space": "cosine"} ) collection.add( ids=[f"chunk_{i}" for i in range(len(chunks))], documents=chunks, metadatas=[{"source": "product_guide.md", "index": i} for i in range(len(chunks))] )

这里有几个关键点:

  • get_or_create_collection会优先获取已有集合,不存在才创建,这个写法天然支持幂等操作。
  • ids必须唯一,写入一批数据时就像是给每条记录发了一张身份证。
  • metadatas必须和documents一一对应,否则会错位。如果某条没有额外信息,用空字典也可以。

如果之后想再加新文档,继续用collection.add就行,Chroma 会负责把它们合并进索引。

3.4 查询与相似度分数的正确打开方式

写入之后,检索是重头戏:

results = collection.query( query_texts=["如何配置持久化模式?"], n_results=5, where={"source": "product_guide.md"} ) for doc in results["documents"][0]: print(doc)

query_texts传入问题,Chroma 会用集合绑定的嵌入函数自动对问题做向量化,不要求你手动提前转换。n_results控制召回条数,where是元数据过滤条件,这个参数在知识来源多、类型杂的系统里尤其好用。

返回结果里有一项distance,很多人会在这里踩坑。使用 cosine 距离时,Chroma 返回的是1 - 余弦相似度,所以数值越小代表越相似,0 表示完全一致,接近 1 表示基本不相关。把它当成百分比只会得出完全错误的结论。

我都习惯这样换算:

similarity = 1 - distance print(f"相似度:{similarity:.4f}")

不同嵌入模型产生的距离分布差异很大,务必在自己数据集上先抽样看一遍分数区间,再决定“低于多少就不送入大模型”的阈值。这个阈值太松,会带进一堆无关文本干扰模型;太紧,又可能漏掉正确答案。

4. 从 Demo 走向生产:配置、过滤与性能

4.1 持久化与数据备份策略

生产环境里,搭建客户端必须走持久化模式:

client = chromadb.PersistentClient(path="/data/chroma_store")

数据目录建议放在独立磁盘上,不要和系统盘放一起。我还建议从落地第一天就把数据管理当成正经事:

  • 每天对chroma_store做一次定时增量备份。
  • 写入时给每个块打上ingest_time和source_version元数据,方便对特定版本的数据做追溯和重建。
  • 上线前提前测试“从备份恢复”的操作,而不是等数据出问题才临时研究。

Chroma 的持久化文件结构很直观,目录下有 SQLite 元数据文件和向量索引子目录。这两块东西完整,整个库基本就能恢复回来。

4.2 元数据方案设计是检索精度的隐藏杠杆

元数据看起来不起眼,但它是检索质量的隐形决定因素。我一开始没在意,写数据时随便填几个字段,后来检索结果乱七八糟,才意识到元数据设计有多重要。

一套合格的元数据至少要回答这几个问题:

  • 这段内容来自哪份文档?
  • 文档是什么类型,比如操作手册、开发文档、售后常见问题?
  • 内容属于哪条业务线或哪个知识域?
  • 写入时间和版本号是多少?

举个例子,同一个库里既有产品操作手册,又有售后报错记录,用户问“系统启动报错了怎么办?”时,售后记录明显更相关。这时候靠元数据先过滤掉操作手册类的内容,再去做向量召回,精度提升是肉眼可见的。

results = collection.query( query_texts=["系统启动报错怎么办"], n_results=10, where={"doc_type": "troubleshooting"} )

这样的过滤条件能把最不相关的候选先挡在门外,把有限的上下文窗口留给真正有价值的内容。

4.3 向量召回不够时,混合检索来补

纯向量检索有一个天然盲区:字面完全一致但语义不同的内容,以及语义相近但字面完全不重合的内容。前者容易误召,后者容易漏召。

我在生产系统里用的方案是混合检索。简单说就是:向量召回一批结果,再用关键词检索召回同一批文档的结果,两边合并、去重,最后用重排序模型打分。

Chroma 自身的定位是向量库,不是全文检索引擎,所以关键词检索那一路可以交给轻量级的 FTS5,或者更重型的全文检索服务,在业务代码里做合并。架构大致长这样:

  1. 问题进入系统后,同时触发向量检索和关键词检索。
  2. 两路检索各自返回 top-N 候选。
  3. 候选合并去重,形成一份更大的候选池。
  4. 用bge-reranker这类跨编码模型,把“问题 + 候选内容”拼接起来算相关性得分。
  5. 按得分重新排序,取前 k 个片段送进 Prompt。

重排序模型通常比纯向量相似度准得多,但推理成本更高,所以只适合对候选池做精排,不适合全库跑。我在项目中用的经验值是:向量召回 top-50,重排后取 top-5 进 Prompt。

4.4 并发访问与接口封装

Chroma 进程内模式天然不适合高并发直查。它是嵌入式库,所有调用在同一个 Python 进程里排队执行,对外直接裸用,并发一大就把进程卡死。

通常的解法是在外面包一层接口服务。我用 FastAPI 封装了一个简单的查询服务,主要代码思路是:

from fastapi import FastAPI from pydantic import BaseModel app = FastAPI() class QueryRequest(BaseModel): question: str top_k: int = 5 @app.post("/rag/query") def query(request: QueryRequest): results = collection.query( query_texts=[request.question], n_results=request.top_k ) return results

封装成 API 之后,调用端就可以与服务解耦。需要并发控制时,再在服务内部加入线程池或者异步调度,避免多个请求同时抢占嵌入式客户端。我实际测下来,这种方式应对内部几十人的并发问答绰绰有余。

4.5 HNSW 参数调优的几个方向

Chroma 默认的索引算法是 HNSW。有两个核心参数需要注意:

  • search_ef:查询时考察的候选数量,值越大越准,但越慢。
  • M:每层最大连接数,影响索引体积和检索速度。

在新建集合时,可以通过元数据指定:

collection = client.get_or_create_collection( name="docs_v2", embedding_function=ef, metadata={ "hnsw:space": "cosine", "hnsw:search_ef": 200, "hnsw:M": 16 } )

不同规模阶段,我的经验是这样的:

  • 万级以下:默认配置完全够用,不用折腾。
  • 十万级:建议显式指定search_ef在 100 到 300 之间。
  • 百万级以上:先考虑数据分片、分库,再考虑放大参数,不建议单集合盲目堆。

参数调优没有银弹,最终还是要基于你自己的数据做线下评测。我习惯先固定M,再用一组基准问题反复测召回率,逐步调整search_ef,直到效果稳定。

5. 实测过程中的高频问题与排查清单

5.1 常见报错与解决办法

我把这些年踩过的问题整理成一张速查表,按频率从高到低排列。

现象原因解决办法
写入时报维度不匹配集合已声明维度,新数据用了另一种维度换新集合,或者确认嵌入模型完全一致
查询结果与问题完全不相关元数据过滤条件写错或太严先放宽过滤,打印实际命中文档数,逐级调整
服务重启后数据消失用了内存模式而非持久化模式统一换成 PersistentClient
中文乱码或截断按字节切分切断了 UTF-8 字符严格按字符边界切分,不要用字节长度
检索延迟突然升高数据量增长,默认索引参数不匹配调search_ef,评估是否需要分集合

5.2 嵌入模型不一致引发的假相似

这是一个特别隐蔽的坑。开发到一半,你觉得当前嵌入模型效果不够好,把它换成了另一个模型,但忘了新建集合。旧集合里的向量是用旧模型生成的,新查询却用新模型向量化,两个模型的向量空间完全不同,算出来的“相似度”毫无数学意义。

我在代码层面直接把这个坑堵死了:集合名称里带上嵌入模型版本号,比如docs_v1_bge_m3。这样只要换模型,就必须新建集合,数据层面不会混用。团队里其他成员看到名称,也能立刻知道当前集合的语义空间是哪一套的。

5.3 上下文塞满低质量片段

这个问题出在 RAG 调度逻辑上,不在 Chroma。如果不管检索分数如何,每次都硬塞 top-k 个片段进 Prompt,当知识库里相关文档很少时,上下文会被低质量片段污染,大模型的回答质量波动会很明显。

我现在的处理方式有三个要点:

  • 检索结果先按相似度分数排序,低于设定阈值的直接丢弃。
  • 片段顺序按“越相关越靠前”重排,让模型优先看到关键证据。
  • 每个片段附上来源元数据一并写入 Prompt,让模型在证据不足时明确输出“我不知道”。

这样做的副作用是,大模型不再硬编答案,整体回答的诚实度反而上来了。

5.4 性能排查的固定顺序

遇到检索慢,不要急着加机器。按照这个顺序排查:

  1. 确认索引确实走的是 HNSW,而不是某些配置错误导致的暴力扫描。
  2. 统计集合内向量总数,数据规模到了一定阈值,性能曲线会陡然变化。
  3. 检查查询条件里是否带了超大范围的元数据过滤,有时过滤条件会引入大量无用扫描。
  4. 确认并发是否过高,嵌入式模式扛不住并发是常态,只能通过架构层解决。
  5. 最后再动search_ef和M参数,并在调参前后跑同一组评测问题做对比。

这套排查顺序我写进了项目组的知识库,每次遇到“突然变慢”的问题,照着走一遍,基本都能快速定位。

6. 一个内部知识问答系统的完整落地复盘

6.1 项目背景和需求

某团队要做内部产品知识问答机器人,素材是几百篇 Markdown 格式的产品文档和售后工单,总字数大概几十万字。需求很具体:回答要准,要能标出引用来源,要支持按产品线过滤,而且所有数据必须留在内网,不能上公网服务。

这个需求映射到技术栈上就是:本地化嵌入模型 + Chroma 做向量存储召回 + 重排序模型精排 + 大模型生成回答。整套链条不需要外部 API,完全内网闭环。

6.2 落地流程拆解

  1. 文档统一转成 Markdown 格式,清洗掉页眉页脚和无关目录。
  2. 按章节标题切块,块与块之间保留约 10% 的重叠。
  3. 用本地化的bge-m3做向量化,进程内持久化写入 Chroma。
  4. 每个块打上product_line、doc_type、source_url三个元数据字段。
  5. 线上查询时,先按产品线元数据过滤,再做向量 top-50 召回。
  6. 用bge-reranker对 top-50 精排,取前 5 个片段。
  7. 低于相似度阈值的片段丢弃,剩余片段按顺序拼入 Prompt。
  8. 大模型生成回答时,强制标注引用来源。

6.3 效果与复盘

这个方案落地后,两个核心指标都有明显变化。回答相关性评测从不到六成提升到了八成以上;检索耗时稳定在可接受范围内。最让我意外的是,很多之前“答非所问”的问题,根本没有换更好的模型,只靠元数据过滤和重排序就纠正了。

复盘时,我们把成功因素总结成三条:分块粒度合理、元数据支撑过滤、重排序把关质量。这三件事扎实之后,向量数据库本身的差异反而不那么显眼。但反过来想,正因为底座足够轻量稳定,我们才有精力把时间花在这些刀刃上。

7. 我对 Chroma 的个人判断和使用建议

项目做多了之后,我对工具的选择标准其实变得很朴素:能不能一分钟上手,关键时刻掉不掉链子,出了问题能不能快速定位。Chroma 在这三项上表现都非常好。它的目标很明确,就是给 RAG 场景一个零摩擦的本地化向量库。它不是万能的分布式数据库,但知道自己适合什么,反而在适合的场景里做得比别人更出色。

如果你现在正处于“刚接触 RAG,想快速验证想法”的阶段,直接上手 Chroma 是性价比最高的选择。如果你已经有一个线上系统,想在内部数据上做私有化知识增强,它也是很好的存储和召回底座。当规模真的迈入百万级文档、高并发在线服务的台阶,再考虑把底座替换成重型分布式方案,或者用混合检索架构搭桥,都是顺理成章的事。

最后分享一个我最近一直保留的习惯:每个 RAG 项目都建一个独立的小测试脚本,只做一件事——随机抽 50 个真实问题,跑一遍完整检索链路,人工判断前五名的召回结果合不合理。每次修改切分参数、嵌入模型或过滤条件之后,都要回来跑一遍这个回归测试。有这个兜底在,后面再怎么折腾架构,心里都不会慌。

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

MySQL Windows服务启动失败1067错误根因与实战修复指南

简介&#xff1a;本资源是一份针对Windows环境下MySQL服务启动失败&#xff08;错误代码1067&#xff09;的实战排错指南&#xff0c;面向数据库初学者、运维人员及开发工程师&#xff0c;聚焦服务无法启动这一高频故障场景。内容基于真实问题排查过程展开&#xff0c;详细还原…

作者头像 李华
网站建设 2026/10/22 13:12:44

函数递归知识

函数递归知识 文章目录函数递归知识一.什么是递归&#xff08;一&#xff09;递归的思想&#xff08;二&#xff09;递归的限制条件二.递归举例&#xff08;一&#xff09;举例1&#xff1a;求n的阶乘1.分析和代码实现2.画图推演&#xff08;二&#xff09;举例2&#xff1a;顺…

作者头像 李华
网站建设 2026/10/20 10:02:39

11_给RAG建baseline_别被看起来不错骗了

给 RAG 建 baseline&#xff1a;别被"看起来不错"骗了 RAG 做到能跑、能答&#xff0c;很容易——跑几条查询&#xff0c;模型答得挺顺&#xff0c;就觉得自己做完了。但没有 baseline&#xff0c;后面所有优化都是自说自话&#xff1a;你不知道自己在进步还是退步&a…

作者头像 李华
网站建设 2026/10/19 4:37:52

游戏引擎基础架构深度解析:从分层设计到资源管理

有朋友私信问我&#xff0c;刚接手一个自研引擎项目&#xff0c;几千个源文件摆在眼前&#xff0c;该从哪里看起。我给的答案一直是同一个&#xff1a;先看引擎的基础架构&#xff0c;也就是引擎的“骨架”。这有点像你拿到一栋复杂的建筑&#xff0c;先得搞清楚承重墙在哪、管…

作者头像 李华
网站建设 2026/10/22 0:22:41

自动测角原理和方法(同时波瓣)

同时波瓣法&#xff0c;单脉冲自动测角系统。即多个波瓣同时存在&#xff0c;理论上可以通过发射一个脉冲测得目标角度信息。单脉冲自动测角系统&#xff0c;主要包括两种方法&#xff1a;1.振幅和差式单脉冲2.相位和差式单脉冲以下介绍振幅和差式单脉冲相关内容振幅和差式单脉…

作者头像 李华