
在 Go 中使用 LangChain 实现文档问答Stuff QA 与 Refine QA 实战指南【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo导读本文基于 langchaingo 仓库中的 document-qa-example 示例完整讲解如何在 Go 程序中基于文档内容构建问答Question Answering能力。你将掌握 langchaingo 提供的两种核心文档问答策略——Stuff QA一次性塞入全部文档与 Refine QA逐文档迭代精炼的用法、适用场景、底层实现原理并了解它们与 MapReduce QA、Retrieval QA 等兄弟方案的关系从而在实际项目中做出正确的技术选型。一、示例概览它在做什么examples/document-qa-example/document_qa.go是一个可直接运行的完整程序它做了三件事初始化一个 OpenAI 大语言模型LLM客户端准备两份关于 Harrison 与 Ankush 教育经历的示例文档分别用Stuff QA Chain与Refine QA Chain对同一批文档提问并打印各自的回答。示例的核心价值在于对比同样的文档、同样的提问类型两种链在提示词构造方式与调用 LLM 的次数上截然不同理解这一差异是选择方案的前提。二、运行前提与依赖运行该示例前需要满足本机已安装 Go示例的 go.mod 声明go 1.24.3并依赖github.com/tmc/langchaingo v0.1.14-pre.4已设置 OpenAI API Key 环境变量代码通过openai.New()默认读取OPENAI_API_KEY在示例目录下执行go run document_qa.go。代码入口main()将错误打印到标准错误流并返回非零退出码符合 Go 命令行程序的惯例。若需自定义模型参数如模型名、温度可通过openai.WithModel(...)、openai.WithTemperature(...)等 Option 传入openai.New()相关实现可参考 llms/openai 包。三、文档问答的基础数据结构schema.Document两种链的输入都依赖统一的文档类型。schema.Document定义于 schema/documents.go字段如下字段类型含义PageContentstring文档正文内容Metadatamap[string]any文档元数据如来源、标题等Scorefloat32文档相关性分数多由检索器填充示例中构造的文档docs : []schema.Document{ {PageContent: Harrison went to Harvard.}, {PageContent: Ankush went to Princeton.}, }对于真实场景文档可以来自 documentloadersCSV、PDF、HTML、Notion 等加载器或向量数据库的检索结果再以[]schema.Document形式喂给问答链。四、Stuff QA小文档集的简单快速方案4.1 用法llm, err : openai.New() if err ! nil { return err } stuffQAChain : chains.LoadStuffQA(llm) answer, err : chains.Call(context.Background(), stuffQAChain, map[string]any{ input_documents: docs, question: Where did Harrison go to collage?, }) if err ! nil { return err } fmt.Println(answer)调用链的核心函数是chains.LoadStuffQA(llm)它接收一个llms.Model返回StuffDocuments链。执行时通过chains.Call传入两个输入键input_documents文档切片与question问题。4.2 底层原理把文档塞进一个提示词从 chains/question_answering.go 的源码可以看到LoadStuffQA内部做了三件事用默认模板_defaultStuffQATemplate构造一个LLMChain模板原文为Use the following pieces of context to answer the question at the end. If you dont know the answer, just say that you dont know, dont try to make up an answer. {{.context}} Question: {{.question}} Helpful Answer:用该LLMChain调用NewStuffDocuments生成 Stuff 链。而StuffDocuments的核心逻辑在 chains/stuff_documents.go从输入值中取出input_documents键对应的[]schema.Document类型不匹配会返回ErrInputValuesWrongType通过joinDocuments把所有文档的PageContent用默认分隔符\n\n拼接成一个长字符串放进提示词变量context连同其他输入如question一起交给内层LLMChain生成回答。也就是说整个文档集只触发一次 LLM 调用这是 Stuff 方案速度快的根本原因。它有三个可配置的默认值均在chains/stuff_documents.go中定义字段默认值说明InputKeyinput_documents外部传入文档所用的输入键DocumentVariableNamecontext拼接后的文档注入提示词的变量名Separator\n\n文档之间的拼接分隔符4.3 适用场景与局限Stuff QA 代码简单、调用快适合文档数量少、总体积小的场景。它的局限也很明显所有文档会被完整拼进提示词一旦文档总量逼近甚至超过模型的上下文窗口就会出现截断或报错。示例 README 也明确指出它 Suitable for a small number of documents。五、Refine QA逐文档迭代精炼5.1 用法refineQAChain : chains.LoadRefineQA(llm) answer, err chains.Call(context.Background(), refineQAChain, map[string]any{ input_documents: docs, question: Where did Ankush go to collage?, }) fmt.Println(answer)5.2 底层原理滚动更新中间答案LoadRefineQA见 chains/question_answering.go构造了两条LLM 链第一条使用 Stuff 风格模板负责基于第一份文档生成初始答案第二条使用精炼模板_defaultRefineQATemplate负责用后续文档逐份改进答案。精炼模板的语义是给定原问题、已有答案与新上下文只在需要时修改已有答案若新上下文无价值则保留原答案。RefineDocuments.Call的迭代过程在 chains/refine_documents.go 中清晰可见校验input_documents类型且文档切片不能为空为空返回ErrInvalidInputValues用第一份文档docs[0]构造初始输入调用Predict得到初始回答对剩余文档for i : 1; i len(docs); i逐个构造精炼输入——把上一轮的回答放入existing_answer变量、当前文档放入context变量——再次调用 LLM滚动更新回答最终把最后一轮的回答放进默认输出键text返回。每次迭代还会把文档的Metadata一并提供给提示词模板默认文档模板为{{.page_content}}变量名page_content便于在需要时引用来源信息。5.3 代价与适用场景由于每份文档都要单独调用一次 LLMRefine QA 的 LLM 调用次数 文档数且各次调用存在前后依赖无法并行示例 README 与源码注释均强调 cant be done in parallel。因此它比 Stuff 慢、也更耗 token但换来的是几乎不受文档总量限制的处理能力适合文档集较大的场景或需要在长文档中逐段累积证据的问答任务。六、横向对比还有哪些文档问答方案除了上述两种langchaingo 的 chains/question_answering.go 还提供了其他方案理解全貌有助于选型方案构造函数调用次数特点Stuff QALoadStuffQA(llm)1 次最快文档体积受上下文窗口限制Refine QALoadRefineQA(llm)N 次顺序无体积上限但慢且不可并行MapReduce QALoadMapReduceQA(llm)N1 次可并行先对每份文档分别抽取相关内容再合并成最终答案MapRerank QALoadMapRerankQA(llm)N 次可并行每份文档独立回答并打分返回得分最高的答案其中 MapReduce 的并行抽取由 chains/map_reduce.go 的Apply实现默认并发数为 5_defaultApplyMaxNumberWorkers可通过MaxNumberOfConcurrent字段调整。另外若你的文档不是手工准备而是来自检索器可以直接使用 chains/retrieval_qa.go 的NewRetrievalQAFromLLM(llm, retriever)它内部就是用LoadStuffQA作为合并文档链先由Retriever.GetRelevantDocuments取回相关文档再执行问答并把检索到的文档通过source_documents键回传需将ReturnSourceDocuments设为true。七、测试验证代码行为有据可查仓库用 httprr 录制回放的方式对上述链做了测试见 chains/question_answering_test.goTestRefineQA调用LoadRefineQA并断言结果中存在text键TestMapReduceQA调用LoadMapReduceQA并断言答案中包含 LeoTestMapRerankQA当前标记为跳过t.Skip注释指向 issue #415这提醒我们 MapRerank 方案在仓库中仍属于试验性质生产使用前需自行验证。这些测试证明只要设置了OPENAI_API_KEY示例的调用模式即可真实跑通也为我们阅读chains.Call的输入校验逻辑见 chains/chains.go缺键会返回ErrMissingInputValues提供了最直接的参考。八、如何接入真实项目将示例落地到业务项目时通常遵循以下演进路径文档准备用 documentloaders 加载业务文档或将向量库检索结果转为[]schema.Document方案选型文档少且短 →LoadStuffQA文档多或单文档很长 →LoadRefineQA或LoadMapReduceQA以利用并行已有检索器 →NewRetrievalQAFromLLM定制提示词默认模板已内置不知道就直说、不要编造的反幻觉约束。需要定制时可仿照LoadStuffQA的实现方式用prompts.NewPromptTemplate自定义模板后再通过NewStuffDocuments/NewRefineDocuments自行组装链结果消费chains.Call返回map[string]any其中text键即为最终答案。示例项目通过 go.mod 以独立 module 依赖github.com/tmc/langchaingo可将其作为最小可运行模板替换文档与问题后即可快速验证思路。结语本文以examples/document-qa-example为骨架完整拆解了 Stuff QA 与 Refine QA 的用法、默认提示词、底层调用链与性能特征并横向对照了 MapReduce、MapRerank 与 Retrieval QA 方案。核心结论一句话小文档集用 Stuff大文档集用 Refine或并行的 MapReduce需要自动检索时用 Retrieval QA——结合 chains/question_answering.go、chains/stuff_documents.go 与 chains/refine_documents.go 的源码即可在 Go 中稳健地构建自己的文档问答能力。【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考