LangChain Go 中基于 Ollama 构建 Agent 的实战指南:MRKL 输出解析容错与提示工程最佳实践
【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo
本文以 langchaingo 仓库中的 Ollama Agent 使用指南 为核心,系统讲解在 Go 项目中使用 Ollama 模型驱动 MRKL Agent 的原理、配置与排障方法。文章将结合仓库源码,重点剖析针对 Ollama 模型没有原生 Function Calling 能力而引入的parseOutput容错解析逻辑,并给出可直接运行的完整代码示例与测试验证方案。读完本文,你将掌握如何在 langchaingo 中为 Ollama 模型配置稳定的 Agent 工作流,并具备独立排查 "unable to parse output" 等常见问题的能力。
背景:为什么 Ollama 模型需要特殊处理
OpenAI 等模型原生支持 function/tool calling,模型输出会按照约定的 JSON 结构返回工具调用参数,程序可以直接消费。而 Ollama 本地模型通常不具备这种原生能力,Agent 必须依赖 MRKL(Modular Reasoning, Knowledge and Language)式的纯文本协议进行推理:
- 模型需要输出
Thought:(思考)、Action:(动作)、Action Input:(动作输入)来表达"我要调用哪个工具"; - 当推理完成时,模型需要输出
Final Answer:来表达"最终答案"。
一旦模型输出的格式略有偏差(例如大小写不一致、冒号后多了空格、用了 "The answer is:" 这类自然语言表达),Agent 的解析器就可能直接报错。这正是仓库中 Issue #1045 描述的核心问题:Ollama 模型与 Agent 搭配时,模型可能不按预期格式生成响应,导致解析错误。
MRKL Agent 与解析器的容错改进
解析器改进概览
针对上述问题,langchaingo 对 MRKL Agent 的parseOutput函数做了增强,使其能够更灵活地识别多种输出格式。改进点集中在 mrkl.go 的parseOutput方法上:
- "Final Answer" 的大小写变体识别:不仅支持标准的
Final Answer:,还支持final answer:、final answer :(冒号前多空格)等变体; - 自然语言变体识别:支持
the final answer is:、the answer is:等表达; - 大小写不敏感的 Action 模式匹配:
Action:与Action Input:均可忽略大小写。
源码级解析流程
从源码看,parseOutput的判定顺序是(agents/mrkl.go):
第一步:标准格式向后兼容检查。先直接查找大写Final Answer:(常量_finalAnswerAction),命中即取最后一个分段作为最终答案并返回AgentFinish,同时把输出记录到Log字段。
第二步:大小写不敏感变体扫描。将模型输出转为小写后,依次匹配以下变体:
| 变体模式 | 示例 |
|---|---|
final answer: | final answer: 42 |
final answer : | final answer : 42 |
the final answer is: | the final answer is: 42 |
the answer is: | the answer is: 42 |
这里有一个重要的保护逻辑:提取出的答案如果后面还跟着\naction:,说明模型并没有真正结束(它可能还在规划下一步工具调用),此时不会误判为最终答案,而是继续走 Action 解析。
第三步:Action/Action Input 模式匹配。使用正则(?i)Action:\s*(.+?)\s*Action\s+Input:\s*(?s)(.+)进行大小写不敏感、跨行((?s)使.匹配换行)的匹配,把模型输出解析为AgentAction{Tool, ToolInput}。
第四步:向后兼容回退。若上面的正则没有命中,再尝试仓库原有的正则Action:\s*(.+)\s*Action Input:\s(?s)*(.+);仍失败则返回ErrUnableToParseOutput包装的错误(错误类型定义见 errors.go)。
测试用例佐证
仓库中的 markl_test.go 通过TestMRKLOutputParser覆盖了这些解析路径:
Action: foo Action Input: bar→ 解析出工具foo、输入bar;- 多行输入
Action: foo\nAction Input:\nbar\nbaz→ 工具输入为bar\nbaz; - 带
Observation:尾缀的输入Action: calculator\nAction Input: 5 + 3\nObservation:→ 工具calculator、输入5 + 3\nObservation:(该尾缀由 Executor 在调用工具时通过strings.TrimSuffix清理,见 executor.go)。
这些测试说明:解析器改进的目标是让"格式上不够规整"的模型输出也能被正确理解,从而显著降低 Ollama 模型的 Agent 使用门槛。
最佳实践:让 Ollama Agent 稳定工作
1. 使用清晰的系统提示词
Ollama 模型遵循提示词的指令程度较高,因此在创建 Agent 时,应在系统提示词中明确写出期望的输出格式,把Thought/Action/Action Input/Observation/Final Answer的结构逐行交代清楚:
systemPrompt := `You are a helpful assistant that uses tools to answer questions. IMPORTANT: You must follow this exact format: For using a tool: Thought: [your reasoning] Action: [tool name] Action Input: [tool input] For final answer: Thought: I now know the final answer Final Answer: [your answer] Always use "Final Answer:" to indicate your final response.` agent := agents.NewOneShotAgent( ollamaLLM, tools, agents.WithSystemMessage(systemPrompt), )需要说明的是:在当前仓库源码中,WithSystemMessage是通过agents.NewOpenAIOption().WithSystemMessage(...)暴露给 OpenAI Functions Agent 的(见 options.go);对于 MRKL Agent(NewOneShotAgent),提示词模板由前缀、格式指令、后缀三部分组成(见 mrkl_prompt.go),你可以用 WithPromptPrefix、WithPromptSuffix、WithPromptFormatInstructions以及WithPrompt选项来定制系统级指令。无论采用哪种方式,核心原则一致:把期望格式写进提示词,模型输出就越规整。
2. 选择合适的模型
不同 Ollama 模型对格式指令的遵从度差异明显,官方指南 给出了经验性建议:
- 推荐使用:llama3、mistral、mixtral、gemma2 —— 这些模型对 MRKL 格式理解较好;
- 可能需要调优:llama2、codellama —— 需要更强的提示词约束;
- 需要充分测试:phi 等小参数模型 —— 输出格式稳定性较弱,务必用测试用例验证后再上生产。
3. 调整温度等采样参数
降低温度(Temperature)通常能显著提升输出格式的一致性。langchaingo 的 Ollama 实现支持在创建 LLM 时直接传入底层采样参数:
llm, err := ollama.New( ollama.WithModel("llama3"), ollama.WithOptions(ollama.Options{ Temperature: 0.2, // Lower temperature for more consistent formatting }), )从源码看,这些参数最终会被映射到 Ollama 的生成选项上:NumPredict对应MaxTokens、Temperature直接透传、Stop对应停止词、TopK/TopP、Seed、重复惩罚等也一一对应(见 ollamallm.go)。除了Temperature,仓库还提供了大量 Runner 级与采样级选项,例如 WithRunnerNumCtx(上下文窗口,默认 2048)、WithPredictRepeatLastN、WithPredictMirostat等,均可按需组合。
4. 处理格式变体
得益于改进后的解析器,以下格式现在都能被正确识别为"最终答案":
Final Answer: X(标准格式)final answer: X(全小写)The answer is: X(自然语言表达)Answer: X(简化表达)
同理,Action Input:与Action input:等各种大小写组合都能被识别。这四种变体与 Action 的大小写容错,构成了 Ollama Agent 稳定性的第一道防线。
5. 完整示例实现
将以上最佳实践组合起来,即可得到一个完整的可运行程序:
package main import ( "context" "fmt" "log" "github.com/tmc/langchaingo/agents" "github.com/tmc/langchaingo/llms/ollama" "github.com/tmc/langchaingo/tools" ) func main() { // Create Ollama LLM with appropriate settings llm, err := ollama.New( ollama.WithModel("llama3"), ollama.WithOptions(ollama.Options{ Temperature: 0.2, NumPredict: 512, }), ) if err != nil { log.Fatal(err) } // Create tools calculator := tools.Calculator{} // Create agent with clear instructions systemPrompt := `You are a helpful math assistant. Use the calculator tool for computations. Format your responses as: - For calculations: "Action: calculator" then "Action Input: [expression]" - For final answers: "Final Answer: [result]"` agent := agents.NewOneShotAgent( llm, []tools.Tool{calculator}, agents.WithSystemMessage(systemPrompt), agents.WithMaxIterations(5), ) // Create executor executor := agents.NewExecutor( agent, agents.WithMaxIterations(5), ) // Run the agent result, err := executor.Call( context.Background(), map[string]any{ "input": "What is 25 * 4?", }, ) if err != nil { log.Printf("Error: %v", err) } else { fmt.Printf("Result: %v\n", result["output"]) } }示例中的 Calculator 工具使用 starlark 求值器执行数学表达式,其Name()为calculator、Description()描述了输入格式,MRKL 提示词会通过toolNames/toolDescriptions自动将工具清单注入模板(见 mrkl_prompt.go)。
理解底层执行流程
要让上面的示例真正跑通,还需要理解 Agent 的迭代执行机制。NewExecutor返回的 Executor 是负责运行 Agent 的链式组件,其Call方法按MaxIterations次循环执行(executor.go):
- 调用
Agent.Plan生成下一步计划(动作列表或结束信号); - 若返回
finish(最终答案),立即返回结果; - 若返回动作列表,则逐个执行:
doAction会按大写形式在工具映射表中查找工具(getNameToTool将工具名统一转为大写),找不到时把 "X is not a valid tool, try another one" 作为 Observation 反馈给模型,让它重新选择(executor.go); - 每次工具执行的 Observation 会通过
constructMrklScratchPad拼接进agent_scratchpad,形成下一轮推理的上下文(mrkl.go); - 若迭代耗尽仍未产出
Final Answer,返回ErrNotFinished("agent not finished before max iterations")。
另外,MRKL Agent 在调用 LLM 时还注入了停止词\nObservation:,避免模型"抢答"工具结果(mrkl.go)。默认最大迭代次数为 5(initialize.go),可通过 WithMaxIterations 调整。
故障排查
报错 "unable to parse output"
- 原因:模型输出与期望格式不匹配,
parseOutput的所有分支都未命中。 - 解决方案(按优先级):
- 降低温度,减少随机性,让模型更保守地遵循格式;
- 换用能力更强的模型(如 llama3、mixtral);
- 在系统提示词中补充示例,展示"思考→动作→观察→最终答案"的完整链路;
- 考虑 few-shot prompting,给出 1~2 组正例。
- 进阶:Executor 支持通过
WithParserErrorHandler挂载解析错误处理器(options.go)。解析失败时,错误信息会被格式化为 Observation 追加到上下文,让模型在下一轮自行修正(executor.go),这比直接终止更有利于自愈。
报错 "agent not finished before max iterations"
- 原因:模型始终没有生成 "Final Answer",迭代次数耗尽。
- 解决方案:
- 在系统提示词中明确写出
Final Answer:,并说明何时该输出它; - 临时调大
MaxIterations(如 8~10)用于调试,观察模型是否只是"步子迈得慢"; - 检查模型是否在输出本文解析器已支持的变体(如
The answer is:),确认是否有其他格式问题。
- 在系统提示词中明确写出
模型不断重复动作
- 原因:模型不理解"拿到工具结果后就应该停止并给出最终答案"。
- 解决方案:
- 在提示词中加入明确的停止条件,例如"一旦得到计算结果,立即用 Final Answer 回复";
- 在系统提示词中给出完整示例,展示"动作→观察→最终答案"的完整闭环;
- 考虑编写自定义输出解析器,对模型行为做更严格的约束。
用测试验证你的 Agent 配置
将提示词与参数调整好后,建议用自动化测试固化验证,防止后续改动回归。以下是 官方指南 提供的测试骨架:
// Test function to verify Ollama agent works correctly func TestOllamaAgent(t *testing.T) { ctx := context.Background() llm, err := ollama.New( ollama.WithModel("llama3"), ) require.NoError(t, err) calculator := tools.Calculator{} agent := agents.NewOneShotAgent( llm, []tools.Tool{calculator}, agents.WithMaxIterations(3), ) executor := agents.NewExecutor(agent) testCases := []struct { input string expected string }{ {"What is 2+2?", "4"}, {"Calculate 10*5", "50"}, {"What is 100 divided by 4?", "25"}, } for _, tc := range testCases { result, err := executor.Call(ctx, map[string]any{ "input": tc.input, }) if err != nil { t.Logf("Warning: %s failed: %v", tc.input, err) continue } output := fmt.Sprintf("%v", result["output"]) if !strings.Contains(output, tc.expected) { t.Errorf("Expected %s in output, got: %s", tc.expected, output) } } }测试中require来自github.com/stretchr/testify,这是 langchaingo 仓库测试中广泛使用的断言库(可参考 markl_test.go 与各包的*_test.go)。运行前请确保本地已启动 Ollama 服务并拉取对应模型(如ollama pull llama3);若网络环境无法访问模型仓库,也可以借助仓库内部使用的 httprr 录制回放机制将真实请求录制下来做离线回归测试。
总结
为了让 Ollama 模型在 Agent 场景下可靠工作,langchaingo 从两个层面做了工程化处理:
- 解析层容错:改进
parseOutput,支持 "Final Answer" 大小写变体、The answer is:等自然语言表达,以及大小写不敏感的 Action 匹配,并配套TestMRKLOutputParser测试用例(mrkl.go、markl_test.go); - 使用层最佳实践:清晰的系统提示词、合适的模型选型、降低温度、配合
MaxIterations与解析错误处理器进行容错。
诚然,这些改进让 Ollama 模型在使用 Agent 时更可靠,但相比原生支持 function calling 的模型,它仍然依赖谨慎的提示词工程。建议在实际项目中:先用小规模测试用例验证模型对格式的遵从度,再逐步放开业务场景;遇到格式问题优先从"提示词是否明确"与"采样参数是否激进"两个方向排查。本文涉及的源码、测试与完整指南均可直接在 agents 目录 与 llms/ollama 目录 中继续深入阅读。
【免费下载链接】langchaingoLangChain for Go, the easiest way to write LLM-based programs in Go项目地址: https://gitcode.com/GitHub_Trending/la/langchaingo
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考