【本文正在参加金石计划附加挑战赛——第一期命题】
什么是LangChain?
LangChain 是一个用于构建基于大语言模型(LLM)的应用程序的框架。它为开发者提供了一组工具和模块,以简化与语言模型的集成和应用开发。LangChain 的核心功能包括:
-
链式调用:LangChain 允许将多个操作链接在一起,例如将输入传递给语言模型,然后将输出用于进一步处理或与其他数据源交互。
-
多种语言模型支持:框架支持多种大语言模型,开发者可以方便地切换和组合不同的模型,以满足特定需求。
-
数据集成:LangChain 提供了与各种数据源(如数据库、API、文件系统等)的集成,方便开发者从多种渠道获取和处理数据。
-
上下文管理:框架支持在对话或任务中管理上下文信息,以提高模型的响应质量和准确性。
-
扩展性:LangChain 允许开发者自定义和扩展功能,以适应特定的应用场景。
LangChain 的设计理念是降低与大语言模型交互的复杂性,使得开发者可以更专注于构建实际应用,而不是处理底层的技术细节。
LangChainGo的架构模式
LangChain 原本是使用 Python 开发的,LangChain for Go 是 LangChain 框架的 Go 版本实现,旨在为 Go 开发者提供构建基于大语言模型(LLM)应用的工具和功能。与 Python 版本相比,LangChain for Go 旨在将链式调用、数据集成和上下文管理等概念带入 Go 生态系统,使开发者能够轻松构建和扩展 LLM 应用。
核心架构
框架设计
- 接口驱动设计:每个主要组件都通过接口定义,以实现模块化和可测试性。
- 组件架构:模型、链、内存、代理和工具之间清晰分离
- Go 特有的模式:充分利用 Go 的优势,例如接口、goroutine 和显式错误处理。
执行模型
- 上下文传播:所有操作均用于
context.Context取消和超时。 - 错误处理:针对不同故障模式,采用类型化错误进行显式错误处理
- 并发性:原生支持使用
goroutine和channel进行并发操作。 - 资源管理:正确的清理和资源管理模式
语言模型
模型抽象
Model 接口提供了一种与不同 LLM 提供商进行交互的统一方式:
- OpenAI、Anthropic、Google AI 和本地模型之间采用一致的 API
- 支持文本、图像和其他内容类型的多模态功能
- 通过功能选项实现灵活配置
- 可通过类型断言访问的提供商特定功能
沟通模式
- 请求/响应:与LLM的标准同步通信
- 流媒体:实时响应流媒体,提升用户体验
- 批量处理:高效处理多个请求
- 速率限制:内置退避和重试机制
内存管理和状态
内存类型
- 缓冲区内存:存储完整的对话历史记录
- 窗口内存:维护最近消息的滑动窗口
- 令牌缓冲区:根据令牌数量限制管理内存
- 摘要记忆:自动总结之前的对话
状态持久性
- 用于开发和测试的内存存储
- 适用于简单应用程序的基于文件的持久化
- 生产应用程序的数据库集成
- 通过接口实现自定义存储后端
代理和自主性
代理架构
智能体将推理与工具使用相结合:
- 决策制定:LLM 决定使用哪些工具。
- 工具集成:与外部 API 和功能无缝集成
- 执行循环:迭代推理-行动-观察循环
- 内存集成:在多个工具调用之间保持上下文
工具系统
- 内置常用操作工具(计算器、网页搜索、文件操作)
- 通过简洁的界面创建自定义工具
- 复杂操作的工具组合
- 错误处理和超时管理
生产方面考虑
性能
- HTTP 客户端的连接池
- 响应和嵌入的缓存策略
- 使用 goroutine 进行并发处理
- 内存高效的流式操作
可靠性
- 外部 API 调用的熔断模式
- 服务不可用时实现优雅降级
- 全面的错误处理和恢复
- 健康检查和监测集成
安全
- 安全的 API 密钥管理
- 输入验证和清理
- 敏感数据的输出过滤
- 速率限制和滥用保护
这些概念构成了使用 LangChainGo 构建健壮、可扩展应用程序的基础。每个概念都建立在 Go 语言的优势之上,同时提供了各种人工智能应用所需的灵活性。
使用LangChainGo构建LLM应用程序
创建一个工程并安装依赖
首先打开 Goland,创建一个工程,然后安装依赖:
go get github.com/tmc/langchaingo
编写一个简单的 main 函数:
package main
import (
"context"
"fmt"
"log"
"github.com/joho/godotenv"
"github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/llms/openai"
)
func main() {
// 加载 .env 文件
err := godotenv.Load()
if err != nil {
fmt.Println("Error loading .env file")
return
}
ctx := context.Background()
llm, err := openai.New()
if err != nil {
log.Fatal(err)
}
prompt := "What would be a good company name for a company that makes colorful socks?"
completion, err := llms.GenerateFromSinglePrompt(ctx, llm, prompt)
if err != nil {
log.Fatal(err)
}
fmt.Println(completion)
}
在项目根目录创建 .env 文件:
OPENAI_API_KEY=your api key
这个文件里面存放你的 OPENAI_API_KEY,Api Key 从 OpenAI 官方网站申请。然后使用 godotenv 库解析 .env 文件,但是你首先得安装这个库:
go get github.com/joho/godotenv
输出得到:
Socktastic
其他示例
其他的 langchaingo 库的用法参考源码:
OpenAI兼容接口
下面演示如何使用 LangChainGo 调用国产大模型的 OpenAI 兼容接口。你可以使用任何实现了 OpenAI 接口规范的国产 AI 大模型,例如:阿里千问、腾讯混元、DeepSeek、字节跳动豆包等。你只需要把 OpenAI 的 BaseUrl、ApiKey、Model 等参数替换即可,这样你可以灵活的替换模型供应商而不需要修改任何代码,十分方便。
创建ApiKey
本文使用的豆包大模型,打开火山引擎官网申请 ApiKey 即可,然后把 BaseUrl 替换成如下地址:
https://ark.cn-beijing.volces.com/api/v3
示例代码如下:
package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"strings"
"github.com/tmc/langchaingo/chains"
"github.com/tmc/langchaingo/llms/openai"
"github.com/tmc/langchaingo/memory"
)
// 最关键的三个参数,在真实项目里面要做成 Yaml 配置项。
const (
EndPoint = "https://ark.cn-beijing.volces.com/api/v3"
ApiKey = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" // 这里替换成你的 Key
AiModel = "doubao-seed-1-8-251228"
)
func main() {
llm, err := openai.New(
openai.WithBaseURL(EndPoint),
openai.WithToken(ApiKey),
openai.WithModel(AiModel),
)
if err != nil {
log.Fatal(err)
}
// Create conversation memory
chatMemory := memory.NewConversationBuffer()
// Create conversation chain
// The built-in conversation chain includes a default prompt template
// and handles memory automatically
conversationChain := chains.NewConversation(llm, chatMemory)
ctx := context.Background()
reader := bufio.NewReader(os.Stdin)
fmt.Println("Advanced Chat Application (type 'quit' to exit)")
fmt.Println("----------------------------------------")
for {
fmt.Print("You: ")
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input)
if input == "quit" {
break
}
// Run the chain with the input
result, err := chains.Run(ctx, conversationChain, input)
if err != nil {
fmt.Printf("Error: %v\n", err)
continue
}
fmt.Printf("AI: %s\n\n", result)
}
fmt.Println("Goodbye!")
}
构建智能体实战
其实,使用 LangChain 不单单可以开发简单的对话机器人服务,还可以用于开发一些指令解析的功能。比如用户输入一个指令或用一句话要实现一个系统的操作,那么系统可以调用 OpenAI 的接口解析这个指令然后做出对应的操作,在笔者的公司内部已经使用了运维机器人来实现自动化运维减少了人力成本。其实这也就是智能体——AI Agent。
下面来介绍如何使用 OpenAI 的 Function Calling 来构建一个天气查询智能体。先来说说什么是 Function Calling。
什么是Function Calling
函数调用(Function Calling)是一种将大模型与外部工具和 API 相连的关键功能,作为自然语言与信息接口之间的“翻译官”,它能够将用户的自然语言请求智能地转化为对特定工具或 API 的调用,从而高效满足用户的特定需求。
-
核心价值:实现大模型与外部工具的无缝衔接,使大模型能够借助外部工具处理实时数据查询、任务执行等复杂场景,推动大模型在实际产业中的落地应用。
-
工作原理:开发者通过自然语言向模型描述函数的功能和定义,模型在对话过程中自主判断是否需要调用函数。当需要调用时,模型会返回符合要求的工具函数及入参,开发者负责实际调用函数并将结果回填给模型,模型再根据结果进行总结或继续规划子任务。
适用场景
Function Calling 适用于以下需要大模型与外部工具协同的场景:
| 场景分类 | 核心特征 | 核心价值 | 典型应用 |
|---|---|---|---|
| 实时数据交互场景 | 需大模型与外部工具协同处理动态信息 | 处理动态信息查询需求 | 天气/股票/航班实时状态查询、数据库检索与 API 数据调用 |
| 任务自动化场景 | 单次函数调用完成操作 | 提升操作效率 | 邮件/消息自动发送、设备控制指令执行(如智能家居开关) |
| 复杂流程编排场景 | 多工具串并联调用 | 跨工具参数传递、子任务依赖关系管理 | 先查天气再发通知等需跨工具传递参数及管理子任务依赖的场景 |
| 智能系统集成场景 | 与业务系统深度耦合 | 实现系统智能化联动 | 智能座舱多设备联动控制、企业级 Bot 工作流(如飞书会议创建→群组管理→任务生成) |
Function Calling工作原理
想要了解 Function Calling 的工作原理,可以看下面这张图:
Tool接口
LangChainGo 库中提供了一个 Tool 接口,定义了工具对象的规范:
// Tool is a tool for the llm agent to interact with different applications.
type Tool interface {
Name() string
Description() string
Call(ctx context.Context, input string) (string, error)
}
LangChainGo 库已经已经提供了一些工具:
- 计算器:执行数学计算
- 网络搜索:在互联网上搜索信息
- 文件操作:读取、写入和修改文件
- 数据库查询:执行数据库操作
- API 调用:向外部服务发出 HTTP 请求
你可以定义一个结构体实现 Tool 接口,然后注册到智能体中。例如,我定义了一个天气查询的工具:
import (
"context"
"encoding/json"
"fmt"
)
type WeatherInput struct {
City string `json:"city"`
Temperature float64 `json:"temperature"`
Condition string `json:"condition"`
}
type WeatherTool struct{}
func (t *WeatherTool) Name() string {
return "get_weather"
}
func (t *WeatherTool) Description() string {
return `这是一个天气查询工具。请以JSON格式返回如下信息,结构如下:
{ "city": "Beijing" }
`
}
func (t *WeatherTool) Call(ctx context.Context, input string) (string, error) {
var weatherInput WeatherInput
if err := json.Unmarshal([]byte(input), &weatherInput); err != nil {
return "", fmt.Errorf("invalid input format: %v", err)
}
city := weatherInput.City
return fmt.Sprintf(
"%s 当前温度 %.1f°C,风速 %.1f km/h",
city,
15.5,
20.5,
), nil
}
在 main 函数中创建调用:
func main() {
llm, err := openai.New(
openai.WithBaseURL(EndPoint),
openai.WithToken(ApiKey),
openai.WithModel(AiModel),
)
if err != nil {
log.Fatal(err)
}
// Create tools
agentTools := []tools.Tool{
&WeatherTool{},
}
agent := agents.NewOneShotAgent(llm,
agentTools,
agents.WithMaxIterations(3))
executor := agents.NewExecutor(agent)
question := What's weather like in Nanjing?"
answer, err := chains.Run(context.Background(), executor, question)
fmt.Println(answer)
}
在开发过程中,笔者发现了一个很严重的问题,就是大模型输出的参数不稳定。大模型有很小几率输出了 JSON 字符串,大部分情况都是输出一个 "Nanjing" 字符串,这样就导致了我的 JSON 解析失败,进而导致调用失败没有任何结果。
也有可能是国产的大模型跟 LangChanGo 库的兼容性不太好吧(仅仅是猜测)。
FunctionDefinition
我从官方给的 examples 中找到了一种更稳定的方法,那就是使用 FunctionDefinition,严格定义工具的需要的参数。
示例代码如下:
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"github.com/tmc/langchaingo/llms"
"github.com/tmc/langchaingo/llms/openai"
)
var availableTools = []llms.Tool{
{
Type: "function",
Function: &llms.FunctionDefinition{
Name: "getCurrentWeather",
Description: "Get the current weather in a given location",
Parameters: map[string]any{
"type": "object",
"properties": map[string]any{
"location": map[string]any{
"type": "string",
"description": "The city and state, e.g. San Francisco, CA",
},
"unit": map[string]any{
"type": "string",
"enum": []string{"fahrenheit", "celsius"},
},
},
"required": []string{"location"},
},
},
},
}
最后使用 GenerateContent 方法调用大模型,传入 tools 参数即可,大模型会自动分析可以使用的工具,然后返回给智能体应用,让智能体应用调用工具的 API,最后再把调用的结果丢给大模型,最终大模型总结出语义化的文字给智能体应用。
完整代码:
func updateMessageHistory(messageHistory []llms.MessageContent, resp *llms.ContentResponse) []llms.MessageContent {
respchoice := resp.Choices[0]
assistantResponse := llms.TextParts(llms.ChatMessageTypeAI, respchoice.Content)
for _, tc := range respchoice.ToolCalls {
assistantResponse.Parts = append(assistantResponse.Parts, tc)
}
return append(messageHistory, assistantResponse)
}
// executeToolCalls executes the tool calls in the response and returns the
// updated message history.
func executeToolCalls(ctx context.Context, llm llms.Model, messageHistory []llms.MessageContent, resp *llms.ContentResponse) []llms.MessageContent {
fmt.Println("Executing", len(resp.Choices[0].ToolCalls), "tool calls")
for _, toolCall := range resp.Choices[0].ToolCalls {
switch toolCall.FunctionCall.Name {
case "getCurrentWeather":
var args struct {
Location string `json:"location"`
Unit string `json:"unit"`
}
if err := json.Unmarshal([]byte(toolCall.FunctionCall.Arguments), &args); err != nil {
log.Fatal(err)
}
response, err := getCurrentWeather(args.Location, args.Unit)
if err != nil {
log.Fatal(err)
}
weatherCallResponse := llms.MessageContent{
Role: llms.ChatMessageTypeTool,
Parts: []llms.ContentPart{
llms.ToolCallResponse{
ToolCallID: toolCall.ID,
Name: toolCall.FunctionCall.Name,
Content: response,
},
},
}
messageHistory = append(messageHistory, weatherCallResponse)
default:
log.Fatalf("Unsupported tool: %s", toolCall.FunctionCall.Name)
}
}
return messageHistory
}
func getCurrentWeather(location string, unit string) (string, error) {
// 模拟调用天气 API
weatherResponses := map[string]string{
"北京": "5 and sunny",
"南京": "8 and windy",
}
weatherInfo, ok := weatherResponses[strings.ToLower(location)]
if !ok {
return "", fmt.Errorf("no weather info for %q", location)
}
b, err := json.Marshal(weatherInfo)
if err != nil {
return "", err
}
return string(b), nil
}
func main() {
llm, err := openai.New(
openai.WithBaseURL(EndPoint),
openai.WithToken(ApiKey),
openai.WithModel(AiModel),
)
if err != nil {
log.Fatal(err)
}
ctx := context.Background()
messageContent := llms.TextParts(llms.ChatMessageTypeHuman, "南京今天天气如何?")
messageHistory := []llms.MessageContent{
messageContent,
}
fmt.Println("Querying for weather...")
resp, err := llm.GenerateContent(ctx, messageHistory, llms.WithTools(availableTools))
if err != nil {
log.Fatal(err)
}
messageHistory = updateMessageHistory(messageHistory, resp)
messageHistory = executeToolCalls(ctx, llm, messageHistory, resp)
// 再次向模型发送查询, 本次对话会携带历史对话和工具调用的结果
fmt.Println("Querying with tool response...")
resp, err = llm.GenerateContent(ctx, messageHistory, llms.WithTools(availableTools))
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Choices[0].Content)
}
最终打印出来的结果如下:
Querying for weather...
Executing 1 tool calls
Querying with tool response...
南京今天的气温为8摄氏度,伴有大风天气。
总结
langchaingo 这个库主要目的是简化使用 Go 语言开发 LLM 应用程序的步骤,它提供了基础的结构体、函数将原本调用 OpenAI 的 http 接口的过程封装起来了,最终呈现在用户面前的是一个个对应接口的函数或者方法。用户不需要关心那些 http 接口的具体地址,只需要关系业务逻辑的开发即可,最终实现与现有的业务系统集成。