langchaingo 与 Chroma 向量存储实战指南:从城市数据检索看 Go 语义搜索的完整链路
2026/9/15 12:34:53 网站建设 项目流程

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,单位平方公里)两类元数据,随后执行三种不同的相似度查询并打印结果。

整个示例围绕以下四个阶段展开:

  1. 向量存储创建:通过环境变量(CHROMA_URLOPENAI_API_KEY)配置并连接 Chroma 服务;
  2. 文档写入:向集合添加 13 篇城市文档及其元数据;
  3. 相似度搜索:执行三种带不同选项(数量限制、分数阈值、元数据过滤)的查询;
  4. 结果展示:按查询用例分组输出命中的城市名称。

环境准备与依赖

示例是一个独立的 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_URLChroma 服务地址必须设置,否则返回invalid options: missing chroma URL错误
OPENAI_API_KEYOpenAI API Key,用于文本向量化WithOpenAIAPIKey二选一,二者皆无则报错
OPENAI_ORGANIZATIONOpenAI 组织 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)内部依次完成:

  1. 应用所有选项并做校验:URL 缺失或既无 API Key 也无自定义 Embedder 时返回ErrInvalidOptions
  2. 创建 Chroma 客户端并发送Heartbeat请求,确认服务可达;
  3. 决定 Embedding 函数:若用户通过WithEmbedder注入了自定义embeddings.Embedder,则通过 embedder.go 中的chromaGoEmbedder适配器将其包装为 chroma-go 的EmbeddingFunction(实现EmbedDocumentsEmbedQueryEmbedRecords);否则创建 OpenAI Embedding 函数;
  4. 调用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)的实现要点:

  • 选项约束:若调用时携带了EmbedderScoreThresholdFilters选项,会直接返回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 >= 1000population >= 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)的处理逻辑值得仔细阅读:

  1. 解析选项:若显式传入Embedder选项会报错(Chroma 查询的向量化由创建集合时绑定的 Embedding 函数负责);
  2. 校验分数阈值:阈值必须落在[0, 1]区间,否则返回ErrInvalidScoreThreshold(对应测试 TestSimilaritySearchWithInvalidScoreThreshold,用-0.81.8验证了越界报错);
  3. 若配置了命名空间键,会将命名空间过滤条件与用户过滤器通过$and合并(见getNamespacedFilter),保证查询只命中当前命名空间;
  4. 调用 chroma-go 的collection.Query发起查询,并校验返回的DocumentsMetadatasDistances三者长度一致,否则返回ErrUnexpectedResponseLength
  5. 距离转分数:Chroma 返回的是距离值,langchaingo 统一换算为相似度分数score = 1.0 - distance(COSINE 距离下分数越接近 1 表示越相似),并过滤掉低于阈值的文档;
  6. 返回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 >= 1000population >= 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),只要求实现AddDocumentsSimilaritySearch两个方法,这也是它能够统一接入检索链、并可方便替换为其他后端(如 pgvector、weaviate 等)的原因。

测试验证与注意事项

vectorstores/chroma/chroma_test.go 中的测试通过httprr录制回放机制运行,依赖CHROMA_URLOPENAI_API_KEY环境变量;未设置时测试会静默跳过。无 Docker 环境下,测试还会尝试通过 testcontainers 自动拉起chromadb/chroma:0.4.24容器。

使用 chroma 包时需要注意几个已由源码确认的约束:

  • URL 必须显式提供WithChromaURL与环境变量CHROMA_URL至少其一,否则初始化失败;
  • API Key 与自定义 Embedder 至少其一OPENAI_API_KEYWithEmbedder都缺失时初始化失败;
  • 分数阈值必须落在[0, 1]:越界会直接报错;
  • AddDocuments 不支持查询类选项:携带ScoreThresholdFiltersEmbedder选项调用AddDocuments会返回ErrUnsupportedOptions
  • 命名空间需配合键名使用WithNameSpace仅设置命名空间而nameSpaceKey为空时,AddDocuments会报错(默认键名为nameSpace,正常路径无需关心)。

小结

从本示例可以提炼出在 Go 中接入 Chroma 的完整心智模型:用chroma.New配合选项完成连接与集合创建,用AddDocuments写入带元数据的文档,用SimilaritySearch结合WithScoreThresholdWithFilters实现精度可控、条件可组合的语义检索,最后通过ToRetriever无缝接入问答链。示例中的城市数据虽然简单,但其"元数据 + 数值过滤 + 分数阈值"的组合方式,可以直接迁移到电商商品检索、文档知识库问答、日志异常匹配等真实场景。

【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询