langchaingo 与 Chroma 向量存储实战指南:从城市数据检索看 Go 语义搜索的完整链路
【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo
导读
本文以 langchaingo 仓库中的 Chroma 向量存储示例 为骨架,完整拆解"创建向量存储、写入带元数据的文档、执行带分数阈值与元数据过滤的相似度搜索"这一端到端流程。你不仅能直接跑通示例程序,还能深入 chroma 包 的源码实现,理解距离函数、分数阈值、命名空间与过滤器在底层是如何协作的,从而在自己的 Go 应用中落地语义搜索与 RAG 检索。
示例概览:一个基于城市信息的 Chroma 向量存储
examples/chroma-vectorstore-example/目录下的示例演示了如何使用 langchaingo 的 Chroma 集成:程序向向量存储写入一批城市文档,每篇文档包含城市名称(PageContent)以及人口(population,单位百万)和面积(area,单位平方公里)两类元数据,随后执行三种不同的相似度查询并打印结果。
整个示例围绕以下四个阶段展开:
- 向量存储创建:通过环境变量(
CHROMA_URL、OPENAI_API_KEY)配置并连接 Chroma 服务; - 文档写入:向集合添加 13 篇城市文档及其元数据;
- 相似度搜索:执行三种带不同选项(数量限制、分数阈值、元数据过滤)的查询;
- 结果展示:按查询用例分组输出命中的城市名称。
环境准备与依赖
示例是一个独立的 Go module,其 go.mod 声明了以下关键依赖:
github.com/tmc/langchaingo:langchaingo 主库;github.com/amikos-tech/chroma-go:Chroma 官方 Go 客户端,被vectorstores/chroma包封装使用;github.com/google/uuid:用于生成命名空间与文档 ID。
运行示例需要准备两部分环境:一个可访问的 Chroma 服务(下文给出 Docker 启动方式),以及一个 OpenAI API Key(用于将文本编码为向量)。langchaingo 的 chroma 包 定义了三个环境变量常量,全部可以通过环境变量注入:
| 环境变量 | 用途 | 默认行为 |
|---|---|---|
CHROMA_URL | Chroma 服务地址 | 必须设置,否则返回invalid options: missing chroma URL错误 |
OPENAI_API_KEY | OpenAI API Key,用于文本向量化 | 与WithOpenAIAPIKey二选一,二者皆无则报错 |
OPENAI_ORGANIZATION | OpenAI 组织 ID(可选) | 为空时跳过 |
第一步:创建 Chroma 向量存储
示例通过chroma.New创建存储实例(见 chroma_vectorstore_example.go):
store, errNs := chroma.New( chroma.WithChromaURL(os.Getenv("CHROMA_URL")), chroma.WithOpenAIAPIKey(os.Getenv("OPENAI_API_KEY")), chroma.WithDistanceFunction(chroma_go.COSINE), chroma.WithNameSpace(uuid.New().String()), ) if errNs != nil { log.Fatalf("new: %v\n", errNs) }可用构造选项
所有选项均在 vectorstores/chroma/options.go 中定义,采用函数式选项模式:
| 选项 | 作用 | 默认值 / 说明 |
|---|---|---|
WithChromaURL(url) | 指定 Chroma 服务地址,必填 | 未显式传入时回退读取CHROMA_URL环境变量,仍为空则报错 |
WithOpenAIAPIKey(key) | 指定 OpenAI API Key | 未设置时回退读取OPENAI_API_KEY环境变量 |
WithOpenAIOrganization(id) | 指定 OpenAI 组织 ID | 可选 |
WithDistanceFunction(fn) | 设置向量距离函数 | 默认L2;示例使用COSINE |
WithNameSpace(nameSpace) | 设置命名空间(即 Chroma 集合名) | 默认langchain |
WithEmbedder(e) | 注入自定义 Embedder | 未设置时使用 OpenAI Embedding 函数 |
WithIncludes(includes) | 控制查询返回字段(文档、元数据、距离等) | 默认由内部决定 |
底层发生了什么
chroma.New(chroma.go)内部依次完成:
- 应用所有选项并做校验:URL 缺失或既无 API Key 也无自定义 Embedder 时返回
ErrInvalidOptions; - 创建 Chroma 客户端并发送
Heartbeat请求,确认服务可达; - 决定 Embedding 函数:若用户通过
WithEmbedder注入了自定义embeddings.Embedder,则通过 embedder.go 中的chromaGoEmbedder适配器将其包装为 chroma-go 的EmbeddingFunction(实现EmbedDocuments、EmbedQuery、EmbedRecords);否则创建 OpenAI Embedding 函数; - 调用
CreateCollection获取或创建以命名空间命名的集合。
值得注意的一点:示例中使用了uuid.New().String()作为命名空间,意味着每次运行都会创建全新的随机集合,不会与历史数据互相污染,非常适合演示与测试场景。
第二步:向向量存储添加文档
示例使用AddDocuments批量写入 13 篇城市文档(chroma_vectorstore_example.go):
_, errAd := store.AddDocuments(context.Background(), []schema.Document{ {PageContent: "Tokyo", Metadata: meta{"population": 9.7, "area": 622}}, {PageContent: "Kyoto", Metadata: meta{"population": 1.46, "area": 828}}, {PageContent: "Hiroshima", Metadata: meta{"population": 1.2, "area": 905}}, {PageContent: "Kazuno", Metadata: meta{"population": 0.04, "area": 707}}, {PageContent: "Nagoya", Metadata: meta{"population": 2.3, "area": 326}}, {PageContent: "Toyota", Metadata: meta{"population": 0.42, "area": 918}}, {PageContent: "Fukuoka", Metadata: meta{"population": 1.59, "area": 341}}, {PageContent: "Paris", Metadata: meta{"population": 11, "area": 105}}, {PageContent: "London", Metadata: meta{"population": 9.5, "area": 1572}}, {PageContent: "Santiago", Metadata: meta{"population": 6.9, "area": 641}}, {PageContent: "Buenos Aires", Metadata: meta{"population": 15.5, "area": 203}}, {PageContent: "Rio de Janeiro", Metadata: meta{"population": 13.7, "area": 1200}}, {PageContent: "Sao Paulo", Metadata: meta{"population": 22.6, "area": 1523}}, }) if errAd != nil { log.Fatalf("AddDocument: %v\n", errAd) }AddDocuments 的底层处理
AddDocuments(chroma.go)的实现要点:
- 选项约束:若调用时携带了
Embedder、ScoreThreshold或Filters选项,会直接返回ErrUnsupportedOptions——这些选项仅用于查询场景; - 文档 ID:每篇文档由
uuid.New().String()生成随机 ID(源码注释中标注为 TODO,希望未来使用更有意义的 ID); - 元数据拷贝:通过
maps.Copy深拷贝文档元数据,避免后续写入污染调用方持有的 map; - 命名空间注入:若设置了命名空间且配置了
nameSpaceKey(默认键名为nameSpace),元数据中会额外写入该键值对,用于区分同一集合内的不同命名空间; - 写入集合:调用 chroma-go 客户端的
col.Add(ctx, nil, metadatas, texts, ids)完成向量化入库。
第三步:三种相似度搜索
示例将三种查询组织成统一的用例结构(chroma_vectorstore_example.go),每种用例由名称、查询文本、目标文档数量以及搜索选项构成:
exampleCases := []exampleCase{ { name: "Up to 5 Cities in Japan", query: "Which of these are cities are located in Japan?", numDocuments: 5, options: []vectorstores.Option{ vectorstores.WithScoreThreshold(0.8), }, }, { name: "A City in South America", query: "Which of these are cities are located in South America?", numDocuments: 1, options: []vectorstores.Option{ vectorstores.WithScoreThreshold(0.8), }, }, { name: "Large Cities in South America", query: "Which of these are cities are located in South America?", numDocuments: 100, options: []vectorstores.Option{ vectorstores.WithFilters(filter{ "$and": []filter{ {"area": filter{"$gte": 1000}}, {"population": filter{"$gte": 13}}, }, }), }, }, }三种查询的设计意图分别为:
- Up to 5 Cities in Japan:检索日本城市,最多返回 5 篇,且要求相似度分数不低于 0.8;
- A City in South America:检索南美城市,仅返回最相关的 1 篇,同样设置 0.8 的分数阈值;
- Large Cities in South America:检索南美大城市,通过
$and组合两个数值条件——area >= 1000且population >= 13(百万),不设返回数量上限(100 远大于数据量)。
随后循环执行查询并暂存结果:
results := make([][]schema.Document, len(exampleCases)) for ecI, ec := range exampleCases { docs, errSs := store.SimilaritySearch(ctx, ec.query, ec.numDocuments, ec.options...) if errSs != nil { log.Fatalf("query1: %v\n", errSs) } results[ecI] = docs }SimilaritySearch 的核心实现
SimilaritySearch(chroma.go)的处理逻辑值得仔细阅读:
- 解析选项:若显式传入
Embedder选项会报错(Chroma 查询的向量化由创建集合时绑定的 Embedding 函数负责); - 校验分数阈值:阈值必须落在
[0, 1]区间,否则返回ErrInvalidScoreThreshold(对应测试 TestSimilaritySearchWithInvalidScoreThreshold,用-0.8和1.8验证了越界报错); - 若配置了命名空间键,会将命名空间过滤条件与用户过滤器通过
$and合并(见getNamespacedFilter),保证查询只命中当前命名空间; - 调用 chroma-go 的
collection.Query发起查询,并校验返回的Documents、Metadatas、Distances三者长度一致,否则返回ErrUnexpectedResponseLength; - 距离转分数:Chroma 返回的是距离值,langchaingo 统一换算为相似度分数
score = 1.0 - distance(COSINE 距离下分数越接近 1 表示越相似),并过滤掉低于阈值的文档; - 返回
schema.Document,其中Score字段携带换算后的相似度分数。
过滤器语法
vectorstores.WithFilters接收map[string]any,底层透传给 Chroma 的元数据过滤。示例展示了$and与$gte的组合;从 chroma_test.go 可以看到更多受支持的算子,例如:
$eq:精确相等匹配(如{"location": {"$eq": "patio"}});$in:枚举匹配(如{"location": {"$in": []string{"office", "kitchen"}}});$gte/$lte等数值比较算子。
测试 TestChromaAsRetrieverWithMetadataFilters 还验证了$and组合$eq与$gte的多条件过滤场景。
第四步:结果展示
查询完成后,示例将每类用例的命中城市以逗号分隔打印(chroma_vectorstore_example.go):
fmt.Printf("Results:\n") for ecI, ec := range exampleCases { texts := make([]string, len(results[ecI])) for docI, doc := range results[ecI] { texts[docI] = doc.PageContent } fmt.Printf("%d. case: %s\n", ecI+1, ec.name) fmt.Printf(" result: %s\n", strings.Join(texts, ", ")) }运行示例:Docker 启动 Chroma 并执行
vectorstores/chroma/README.md 提供了完整的本地运行指引。目前 Chroma 仅支持客户端/服务端模式,因此在本地启动服务最便捷的方式是 Docker:
$ docker run -p 8000:8000 ghcr.io/chroma-core/chroma:0.5.0服务就绪后,设置环境变量并直接运行示例:
$ export CHROMA_URL=http://localhost:8000 $ export OPENAI_API_KEY=YourOpenApiKeyGoesHere $ go run ./examples/chroma-vectorstore-example/chroma_vectorstore_example.go预期的输出如下:
Results: 1. case: Up to 5 Cities in Japan result: Tokyo, Nagoya, Kyoto, Fukuoka, Hiroshima 2. case: A City in South America result: Buenos Aires 3. case: Large Cities in South America result: Sao Paulo, Rio de Janeiro可以看到:日本查询返回了 5 座城市且均为日本城市;南美单城查询命中了布宜诺斯艾利斯;带过滤条件的查询则精确命中area >= 1000且population >= 13的圣保罗与里约热内卢,验证了元数据过滤在数值场景下的有效性。
进阶:把 Chroma 接入检索问答链
Chroma 向量存储不仅支持直接查询,还可以通过 vectorstores.ToRetriever 包装为schema.Retriever,接入 langchaingo 的链式调用。chroma 包的测试充分演示了这一用法:
result, err := chains.Run( context.TODO(), chains.NewRetrievalQAFromLLM( llm, vectorstores.ToRetriever(s, 5, vectorstores.WithScoreThreshold(0.8)), ), "What colors is each piece of furniture next to the desk?", )相关测试见 TestChromaAsRetrieverWithScoreThreshold 与 TestChromaAsRetrieverWithMetadataFilterEqualsClause。ToRetriever接受向量存储、返回文档数量与查询选项,其GetRelevantDocuments内部实际调用的仍是SimilaritySearch,因此分数阈值、过滤器、命名空间等选项在 RAG 场景下同样生效。
VectorStore接口本身非常简洁(vectorstores/vectorstores.go),只要求实现AddDocuments与SimilaritySearch两个方法,这也是它能够统一接入检索链、并可方便替换为其他后端(如 pgvector、weaviate 等)的原因。
测试验证与注意事项
vectorstores/chroma/chroma_test.go 中的测试通过httprr录制回放机制运行,依赖CHROMA_URL与OPENAI_API_KEY环境变量;未设置时测试会静默跳过。无 Docker 环境下,测试还会尝试通过 testcontainers 自动拉起chromadb/chroma:0.4.24容器。
使用 chroma 包时需要注意几个已由源码确认的约束:
- URL 必须显式提供:
WithChromaURL与环境变量CHROMA_URL至少其一,否则初始化失败; - API Key 与自定义 Embedder 至少其一:
OPENAI_API_KEY与WithEmbedder都缺失时初始化失败; - 分数阈值必须落在
[0, 1]:越界会直接报错; - AddDocuments 不支持查询类选项:携带
ScoreThreshold、Filters、Embedder选项调用AddDocuments会返回ErrUnsupportedOptions; - 命名空间需配合键名使用:
WithNameSpace仅设置命名空间而nameSpaceKey为空时,AddDocuments会报错(默认键名为nameSpace,正常路径无需关心)。
小结
从本示例可以提炼出在 Go 中接入 Chroma 的完整心智模型:用chroma.New配合选项完成连接与集合创建,用AddDocuments写入带元数据的文档,用SimilaritySearch结合WithScoreThreshold与WithFilters实现精度可控、条件可组合的语义检索,最后通过ToRetriever无缝接入问答链。示例中的城市数据虽然简单,但其"元数据 + 数值过滤 + 分数阈值"的组合方式,可以直接迁移到电商商品检索、文档知识库问答、日志异常匹配等真实场景。
【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考