langchaingo用法详解及源码解析(一)

大模型开发的基本概念:

虽然大模型本身很复杂,但基于大模型开发应用其实很简单:你只需要发送一段话,它就会返回一段话。整个交互过程的核心只是一个 Chat 接口 ------ 输入是自然语言,输出也是自然语言。相比传统系统(比如操作数据库需要掌握 SQL),这正是大模型应用的魅力所在:一切都可以用自然语言完成交互。

与大模型通信的协议

与大模型通信的协议也很简单,就是普通的 HTTP 。不过由于生成内容较慢,通常会使用 SSE(Server-Sent Events) 进行流式返回。这种方式本质上是将 HTTP 响应的 Content-Type 设置为 text/event-stream,响应体会被切分为一段段的数据块,服务端每生成一块就立刻推送给前端,前端逐块展示,从而实现流式效果。

大模型开发框架的核心目标

大模型开发框架的核心目标,就是对这种自然语言交互过程做一层合理封装,简化开发流程,并让各个模块(如提示词、模型、工具)可以自由组合、灵活插拔。举个例子,如果你希望大模型能告诉你当前的时间,模型本身是静态的,它并不知道现在几点,于是你需要给它接一个"获取当前时间"的工具。在框架中实现这个逻辑非常简单:你只需实现一个工具对象,定义好它的描述和参数,绑定给大模型即可。当模型判断自己需要调用该工具时,框架会让程序自动进入工具执行流程并将执行结果发给大模型,大模型获取结果后再继续与用户对话。

工具机制背后的复杂逻辑

这些工具机制背后其实隐藏了不少复杂逻辑,比如:

  • 工具注册时,框架会自动把工具的描述、参数信息等加入提示词;
  • 提示词中会约定:如果大模型想调用工具,必须加上特定标志并遵循参数格式;
  • 框架在接收到模型的回复后,会用正则匹配检查是否触发了工具调用逻辑;
  • 一旦识别出调用请求,就会执行对应工具,并把结果再交还给大模型。

这些过程在底层是通用且重复的,将它们封装进框架,是提升开发效率的关键。

Go 语言主流大模型框架对比

目前 Go 语言中比较主流的大模型开发框架有两个:langchaingo 和字节开源的 eino。这两个框架都还在快速演进中,版本尚不稳定(分别是 v0.1 和 v0.3)。我最开始用这两个框架时分别实现了一个简单的聊天机器人应用,发现它们在设计理念和核心概念上有很大差异。

eino:图结构的终极框架

最直观的感受是:eino 更"重",上手门槛也更高。它采用图结构作为核心抽象,整个系统由"点"和"边"组成。每个点处理一次数据(如提示词、大模型、工具都视为点),数据在点之间通过边流动。每个点职责单一,需要你把应用逻辑拆成多个点,然后用边串联起来。这种结构一开始不太好理解,也比较难调试。

langchaingo:轻量且直觉的 Chain 设计

相比之下,langchaingo 作为 LangChain 的 Go 实现,整体设计更加轻量,思路也更贴近直觉。框架中没有"点"的概念,核心是各种 Chain 对象。每个 Chain 代表一段完整的业务逻辑(或行为链),比如内置的 APIChain 可以完成从用户意图解析到实际 HTTP 请求发送的全过程。你不需要像在 eino 中那样把逻辑拆成若干个点组合起来,Chain 本身就是一个可以复用的小应用,而且多个 Chain 之间还可以组合成更大的逻辑流,形成中间件式的结构。

开发体验对比

从开发体验上讲,langchaingo 封装得较浅,写代码时你仍然能清楚感受到提示词构造、模型调用、输出解析等每一步在做什么。而 eino 封装得更深,开发时不太需要关心细节,但也因此较难理解底层是如何运行的。langchaingo 的源码结构非常清晰,像一次大模型调用的执行链路很容易读懂;而 eino 各个组件虽然单独都不复杂,但整体调用流程因为是一个抽象的图状结构,读懂整个运行机制还是比较困难的。

总的来说,eino 更像它所定义的那样,是一个"终极框架"------ 一站式、严谨、可扩展,适合构建非常复杂的系统;而 langchaingo 则更加轻量、简洁,开发者有更强的掌控感。个人更偏好 langchaingo 这种风格,因此最终选择它作为我的主力框架,并深入阅读了其源码。接下来我会详细拆解 langchaingo 各个模块的设计理念与实现逻辑。

langchaingo 中包含的各个模块:

langchaingo 是目前最主流大模型开发框架 langchain 的 Go 语言版本,设计理念和模块命名都与 langchain 保持一致。它由多个核心模块构成:

1. llms:大模型通用接口

定义了大模型的通用接口 Model,代表一个具体的大模型实例,并内置了多个主流模型的实现,如 OpenAI、Ollama、Anthropic 等。

2. prompts:提示词模板系统

提示词(Prompt)在大模型开发中极其重要,因为在不进行模型微调的情况下,提示词是影响模型输出的唯一手段。prompts 包提供了提示词模板功能,默认基于 Go 标准库的 text/template 实现,支持动态渲染提示词内容后传给大模型

3. memory:上下文记忆管理

用于管理"记忆"的模块。由于大模型本身是无状态的,每一次请求与响应都是独立的,因此用户偏好、历史对话等上下文信息必须由应用层来维护,想要让大模型基于历史记录回答,必须把历史聊天内容一并发给大模型。该包定义了统一的Memory接口和典型实现,方便与其他组件进行集成。

4. tools:外部工具接入

大模型本质上是一个基于统计学的语言模型,收到提示词之后,基于自己训练的数据拟合出概率最高的回答,这个过程是静态且封闭的,它无法实时访问外部世界的知识。例如,模型本身无法直接回答"今天天气如何"这类问题。tools 模块提供了一套通用接口,可以为模型扩展外部工具的能力,并提供了一些内置实现,如调用搜索引擎、计算数学表达式等。

5. chains:核心执行单元

Chain 是 langchaingo 执行逻辑的基本单位。任何一段可复用的逻辑都可以被封装成一个 Chain 对象,例如 LLMChainConstitutionalChainAPIChain 等。Chain 的最大优势是可组合:不同 Chain 可以嵌套、拼接,构建出复杂的业务流程。因此,一个 Chain 本质上就是一个小型的智能应用。

6. agents:智能体接口与 ReAct 执行模式

Agent 主要实现了 ReAct 模式(Reason + Act)。其核心思想是:大模型在收到用户输入后,不直接回答,而是判断是否需要调用工具辅助思考,经过若干次工具调用后,再最终返回答案。Agent 接口核心是 Plan() 方法,决定下一步是调用工具还是生成最终回复。Agent 最终也会被封装为 Chain,实现统一调度。

7. callbacks:全链路事件钩子

定义了 Handler 接口,用于监听框架中各类执行事件,如:

  • 工具调用开始/结束
  • Agent 调度开始/结束
  • Chain 执行开始/结束

此外,还定义了一个关键的回调方法: HandleStreamingFunc(ctx context.Context, chunk []byte), 由于 llms.Modelchains.Chain 接口默认直接返回完整结果,而非流式数据,要实现前端流式输出,必须通过实现 HandleStreamingFunc,将大模型输出按 chunk 分段返回。

8. embeddings:文本向量化接口

该模块提供了文本向量化的通用接口及典型实现(如 OpenAI Embedding API)。向量化是语义搜索、知识检索等下游任务的基础。

9. vectorstores:向量数据库接口

定义了向量数据库的统一接口,包括文档入库与语义相似度搜索能力,内置集成了多种主流向量数据库,如 Weaviate、Pinecone、Pgvector 等。搭配 embeddings 使用,构建 RAG 系统(检索增强生成)非常方便。


除上述主模块外,langchaingo 还包含一些不在主链路中的子模块,用于解决局部问题,就不在这里进行说明了。

llms包核心用法及源码解析:

这是一段很简单的代码,向模型发送提示词,得到返回值,

go 复制代码
func main() {
    ctx := context.Background()
    llm, err := ollama.New(ollama.WithModel("llama3.2"))
    if err != nil {
       panic(err)
    }
    prompt := "你好,你是谁?"
    completion, err := llms.GenerateFromSinglePrompt(ctx, llm, prompt)
    if err != nil {
       panic(err)
    }
    fmt.Println(completion)
}

// GenerateFromSinglePrompt函数实现如下
func GenerateFromSinglePrompt(ctx context.Context, llm Model, prompt string, options ...CallOption) (string, error) {
    msg := MessageContent{
       Role:  ChatMessageTypeHuman,
       Parts: []ContentPart{TextContent{Text: prompt}},
    }

    // 调用模型的 GenerateContent 方法,Model接口也只需要模型实现此方法,
    // 即对一段提示词返回大模型的回复
    resp, err := llm.GenerateContent(ctx, []MessageContent{msg}, options...)
    if err != nil {
       return "", err
    }

    choices := resp.Choices
    if len(choices) < 1 {
       return "", errors.New("empty response from model")
    }
    c1 := choices[0]
    return c1.Content, nil
}

以最常用的 openai client 为例,看一下它是如何实现 GenerateContent 方法的,此方法首先会进行一些数据类型和枚举的转换,将接口中定义的通用结构转化为openai client自己的结构,转化完成之后,会进入此方法:

go 复制代码
func (c *Client) createChat(ctx context.Context, payload *ChatRequest) (*ChatCompletionResponse, error) {
    // 这里判断了是否存在上面所说的流式读取的callback,如果存在则要求模型流式返回,
    // payload.Stream设置为true
    if payload.StreamingFunc != nil {
       payload.Stream = true
       if payload.StreamOptions == nil {
          payload.StreamOptions = &StreamOptions{IncludeUsage: true}
       }
    }

    // json序列化并发送http请求
    payloadBytes, err := json.Marshal(payload)
    if err != nil {
       return nil, err
    }
    body := bytes.NewReader(payloadBytes)
    req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.buildURL("/chat/completions", payload.Model), body)
    if err != nil {
       return nil, err
    }
    // 这个header里面会设置content-type以及用于鉴权的秘钥
    c.setHeaders(req)

    // 实际发送http请求,httpClient默认为http.DefaultClient,所有对象默认情况下都会使用
    // 同一个client,连接默认情况下能得到复用
    r, err := c.httpClient.Do(req)
    if err != nil {
       return nil, err
    }
    defer r.Body.Close()

    // 存在流式回调,则使用parseStreamingChatResponse读取响应
    if payload.StreamingFunc != nil {
       return parseStreamingChatResponse(ctx, r, payload)
    }

    // 不存在流式回调,则直接反序列化响应并返回
    var response ChatCompletionResponse
    return &response, json.NewDecoder(r.Body).Decode(&response)
}

func parseStreamingChatResponse(ctx context.Context, r *http.Response, payload *ChatRequest) (*ChatCompletionResponse,
    error,
) {
    // 基于响应体创建一个 scanner,sse协议规定,每个数据块之间用 \n\n 分隔,故可用scanner分块获取
    scanner := bufio.NewScanner(r.Body)
    responseChan := make(chan StreamedChatResponsePayload)
    go func() {
       defer close(responseChan)
       for scanner.Scan() {
          line := scanner.Text()
          if line == "" {
             continue
          }
          // sse协议规定每一个数据块必须使用 data: 开头,故此处需要去掉前缀
          data := strings.TrimPrefix(line, "data:")
          data = strings.TrimSpace(data)
          // 分块数据为 [DONE] 时表示响应结束,停止读取响应
          if data == "[DONE]" {
             return
          }
          // 每个数据块都是一个json,反序列化后写入管道,进行处理
          var streamPayload StreamedChatResponsePayload
          err := json.NewDecoder(bytes.NewReader([]byte(data))).Decode(&streamPayload)
          if err != nil {
             streamPayload.Error = fmt.Errorf("error decoding streaming response: %w", err)
             responseChan <- streamPayload
             return
          }
          responseChan <- streamPayload
       }
       if err := scanner.Err(); err != nil {
          responseChan <- StreamedChatResponsePayload{Error: fmt.Errorf("error reading streaming response: %w", err)}
          return
       }
    }()
    // 最后所调用的这个函数会读取responseChan管道,每读取到一部分数据,就会调用 
    // StreamingFunc 将结果通过回调写回上游,让上游可以流式输出,
    // 同时还会不断合并得到的数据,最后将完整的数据通过 GenerateContent 方法一次性返回
    return combineStreamingChatResponse(ctx, payload, responseChan)
}

可以看到llms包整体逻辑是非常清晰且简单的,因为Model接口的职责非常单一,就是对各个大模型的chat接口做了一层抽象,内部的实现原理也基本都是发送 http请求,之后通过sse的方式流式读取大模型的返回值

prompts包核心用法及源码解析:

prompts包职责也很单一,只想解决一个问题,就是通过模板渲染提示词,提示词其实就是一段文本,应用层完全可以自己拼接一段字符串,然后发给大模型,但这样提示词的拼接肯定就是五花八门的,没有统一的方案,框架也无法对提示词做统一的处理,并且每个应用都需要自己再实现一遍提示词渲染的逻辑,这对于应用层来说也很麻烦。所以prompts包声明了提示词模板应当实现的接口,同时内置了提示词模板的实现。

定义的接口如下:

go 复制代码
// Formatter is an interface for formatting a map of values into a string.
type Formatter interface {
    Format(values map[string]any) (string, error)
}

// MessageFormatter is an interface for formatting a map of values into a list
// of messages.
type MessageFormatter interface {
    FormatMessages(values map[string]any) ([]llms.ChatMessage, error)
    GetInputVariables() []string
}

// FormatPrompter is an interface for formatting a map of values into a prompt.
type FormatPrompter interface {
    FormatPrompt(values map[string]any) (llms.PromptValue, error)
    GetInputVariables() []string
}

其实这三个接口想表达的是同一件事,就是有一段提示词的模板,模板中肯定有一些文案也有一些需要渲染的变量,需要实现Format方法,使用按map[string]any传入的变量完成渲染,得到渲染后的提示词,虽然Format, FormatMessages, FormatPrompt的返回值略有不同,但核心逻辑都是一样的,就是针对values参数完成渲染,得到渲染后的提示词。同时还要求实现 GetInputVariables 方法,此方法要求返回提示词模板中的变量的名称,框架拿到这些变量名后可以进行验证或其他的统一处理。

可以看下内置的实现:

go 复制代码
// NewPromptTemplate 新建一个提示词模板
func NewPromptTemplate(template string, inputVars []string) PromptTemplate {
    return PromptTemplate{
       Template:       template,
       InputVariables: inputVars,
       TemplateFormat: TemplateFormatGoTemplate,
    }
}

var (
    _ Formatter      = PromptTemplate{}
    _ FormatPrompter = PromptTemplate{}
)

// Format 此处实现了Format方法,返回值为渲染后的字符串
func (p PromptTemplate) Format(values map[string]any) (string, error) {
    // 将直接声明的变量和用户传入的变量合并到一起,统一进入模板渲染
    resolvedValues, err := resolvePartialValues(p.PartialVariables, values)
    if err != nil {
       return "", err
    }
    return RenderTemplate(p.Template, p.TemplateFormat, resolvedValues)
}

// FormatPrompt 此处实现了 FormatPrompt 方法,
// 内部逻辑复用了 Format 方法,只是最后转换为了 StringPromptValue 类型
func (p PromptTemplate) FormatPrompt(values map[string]any) (llms.PromptValue, error) { //nolint:ireturn
    f, err := p.Format(values)
    if err != nil {
       return nil, err
    }

    return StringPromptValue(f), nil //nolint:ireturn
}

// GetInputVariables 返回模板中包含的变量名
func (p PromptTemplate) GetInputVariables() []string {
    return p.InputVariables
}

// RenderTemplate 实际进行模板渲染,默认会使用go标准库的模板引擎
func RenderTemplate(tmpl string, tmplFormat TemplateFormat, values map[string]any) (string, error) {
    formatter, ok := defaultFormatterMapping[tmplFormat]
    if !ok {
       return "", newInvalidTemplateError(tmplFormat)
    }
    return formatter(tmpl, values)
}

var defaultFormatterMapping = map[TemplateFormat]interpolator{ //nolint:gochecknoglobals
    TemplateFormatGoTemplate: interpolateGoTemplate,
    TemplateFormatJinja2:     interpolateJinja2,
    TemplateFormatFString:    fstring.Format,
}

func interpolateGoTemplate(tmpl string, values map[string]any) (string, error) {
    parsedTmpl, err := template.New("template").
       Option("missingkey=error").
       Funcs(sprig.TxtFuncMap()). // 此处为模板内置了一些渲染的函数
       Parse(tmpl)
    if err != nil {
       return "", err
    }
    sb := new(strings.Builder)
    err = parsedTmpl.Execute(sb, values)
    if err != nil {
       return "", err
    }
    return sb.String(), nil
}

可以看到prompts包核心就是进行提示词模板渲染,默认使用text/template包进行渲染。

memory包核心用法及源码解析:

memory包主要用于解决大模型的记忆问题,大模型的chat接口是完全无状态的,每一次发送提示词并得到回复都是彼此独立的,从llms.Model接口也能体现出来,GenerateContent方法并没有要求传输会话id之类的参数,大模型收到用户本次对话内容时根本无从得知用户之前发送了什么,也就是说这些对话的记忆必须由应用层进行维护,想要让大模型根据历史对话内容进行回复,必须得把历史对话内容都放到提示词,然后一并发送给大模型让其进行回复,memory包就是用来解决这个问题的。在schema包中定义了一个通用的Memory接口,以及一个专门用来处理历史对话记录的ChatMessageHistory接口,memory包内置了一些常用的实现,以下是MemoryChatMessageHistory接口的具体定义:

go 复制代码
// Memory chains所用到的memory的通用接口
type Memory interface {
    // GetMemoryKey 获取memory的Key值
    GetMemoryKey(ctx context.Context) string

    // MemoryVariables 获取memory中保存的key的列表
    MemoryVariables(ctx context.Context) []string

    // LoadMemoryVariables 基于传入的键值返回memory包保存的各类记忆
    LoadMemoryVariables(ctx context.Context, inputs map[string]any) (map[string]any, error)

    // SaveContext 将用户的输入以及模型的输出存入记忆
    SaveContext(ctx context.Context, inputs map[string]any, outputs map[string]any) error

    // Clear 清空全部记忆
    Clear(ctx context.Context) error
}

Memory接口来看,整体就是基于一种kv的方式存储记忆,因为记忆可能是多种类型的,比如用户的信息、偏好以及对话的历史记录,不同类型的记忆按照不同的key进行存储,使用LoadMemoryVariables即可通过key获取存储的各种记忆,此处的inputs参数是一个map[string]any,也就是说具体实现的时候可以基于入参的键值动态调整需要返回的记忆,MemoryVariablesGetMemoryKey都用于返回记忆中的key值,只是GetMemoryKey返回值是一个string,而不是一个列表,这两个方法看起来有些重复,GetMemoryKey更适用于那种记忆仅包含历史对话记录的情况,在对话过程中,用户的输入以及大模型的输出则通过SaveContext方法进行保存。

由于最典型也最必要的memory就是历史对话记录,故历史对话记录的存取也单独声明了一个接口:

go 复制代码
// ChatMessageHistory 定义如何存取对话消息
type ChatMessageHistory interface {
    // AddMessage 通用存储消息方法
    AddMessage(ctx context.Context, message llms.ChatMessage) error

    // AddUserMessage 存储用户发送的消息
    AddUserMessage(ctx context.Context, message string) error

    // AddAIMessage 存储ai回复的消息
    AddAIMessage(ctx context.Context, message string) error

    // Clear 清空所有消息
    Clear(ctx context.Context) error

    // Messages 返回所有消息
    Messages(ctx context.Context) ([]llms.ChatMessage, error)

    // SetMessages 覆盖所有旧消息
    SetMessages(ctx context.Context, messages []llms.ChatMessage) error
}

此接口的方法写的还是很清楚的,就是用于存取大模型的对话消息。从定位上看,ChatMessageHistory属于一类特殊的MemoryMemory是更为通用的定义,或者说ChatMessageHistoryMemory记录的键值中的一个值,所以Chain是不会直接操作ChatMessageHistory的,只会统一操作Memory

下面是memory包的一个具体实现,基于这个实现可以更好的认识memory的作用:

go 复制代码
// NewConversationBuffer 此memory就是用来存储对话记录的,不存储其他的记忆信息
func NewConversationBuffer(options ...ConversationBufferOption) *ConversationBuffer {
    return applyBufferOptions(options...)
}

// MemoryVariables 此memory只存储了对话记录故只有一个key,
// m.MemoryKey 的默认值为 'history',表示存储的是历史记录
func (m *ConversationBuffer) MemoryVariables(context.Context) []string {
    return []string{m.MemoryKey}
}

// LoadMemoryVariables 获取存储的记忆值,因为只存储了一个历史对话记录,故直接忽略了inputs参数
func (m *ConversationBuffer) LoadMemoryVariables(
    ctx context.Context, _ map[string]any,
) (map[string]any, error) {
    // 获取存储的全部历史对话记录
    messages, err := m.ChatHistory.Messages(ctx)
    if err != nil {
       return nil, err
    }

    // 如果要求按message原样返回则直接返回messages切片
    if m.ReturnMessages {
       return map[string]any{
          m.MemoryKey: messages,
       }, nil
    }

    // 不返回message则将messages整理为字符串再进行返回
    bufferString, err := llms.GetBufferString(messages, m.HumanPrefix, m.AIPrefix)
    if err != nil {
       return nil, err
    }

    return map[string]any{
       m.MemoryKey: bufferString,
    }, nil
}

// SaveContext 此处保存记忆,即保存历史对话记录
func (m *ConversationBuffer) SaveContext(
    ctx context.Context,
    inputValues map[string]any,
    outputValues map[string]any,
) error {
    // 保存用户提问的消息
    userInputValue, err := GetInputValue(inputValues, m.InputKey)
    if err != nil {
       return err
    }
    err = m.ChatHistory.AddUserMessage(ctx, userInputValue)
    if err != nil {
       return err
    }

    // 保存ai回复的消息
    aiOutputValue, err := GetInputValue(outputValues, m.OutputKey)
    if err != nil {
       return err
    }
    err = m.ChatHistory.AddAIMessage(ctx, aiOutputValue)
    if err != nil {
       return err
    }

    return nil
}

// Clear 清空全部的历史记录
func (m *ConversationBuffer) Clear(ctx context.Context) error {
    return m.ChatHistory.Clear(ctx)
}

// GetMemoryKey 返回自己保存记忆的key
func (m *ConversationBuffer) GetMemoryKey(context.Context) string {
    return m.MemoryKey
}

可以看到ConversationBuffer保存记忆是完全使用自己的ChatHistory属性进行保存的,这个属性如果用户不主动注入,默认会使用一个基于内存的实现来保存历史消息,故只能做一些测试使用,可以看一下内置的一个基于mongodb的ChatMessageHistory接口的实现,用于持久化的保存历史对话记录:

go 复制代码
// NewMongoDBChatMessageHistory 基于mongodb实现的历史消息记录存储
func NewMongoDBChatMessageHistory(ctx context.Context, options ...ChatMessageHistoryOption) (*ChatMessageHistory, error) {
    h, err := applyMongoDBChatOptions(options...)
    if err != nil {
       return nil, err
    }
    client, err := mongodb.NewClient(ctx, h.url)
    if err != nil {
       return nil, err
    }

    h.client = client

    h.collection = client.Database(h.databaseName).Collection(h.collectionName)
    // 不同会话的聊天记录通过session_id进行区分,故此处对文档的session_id字段进行索引,
    // 用于快速定位到某次会话的对话记录
    if _, err := h.collection.Indexes().CreateOne(ctx, mongo.IndexModel{Keys: bson.D{{Key: mongoSessionIDKey, Value: 1}}}); err != nil {
       return nil, err
    }

    return h, nil
}

// Messages 返回某次会话的全部消息
func (h *ChatMessageHistory) Messages(ctx context.Context) ([]llms.ChatMessage, error) {
    // 检索某个会话id的全部消息记录
    messages := []llms.ChatMessage{}
    filter := bson.M{mongoSessionIDKey: h.sessionID}
    cursor, err := h.collection.Find(ctx, filter)
    if err != nil {
       return messages, err
    }

    // 反序列化存储的对话消息并返回
    _messages := []chatMessageModel{}
    if err := cursor.All(ctx, &_messages); err != nil {
       return messages, err
    }
    for _, message := range _messages {
       m := llms.ChatMessageModel{}
       if err := json.Unmarshal([]byte(message.History), &m); err != nil {
          return messages, err
       }
       messages = append(messages, m.ToChatMessage())
    }

    return messages, nil
}

// AddAIMessage 增加ai对话消息
func (h *ChatMessageHistory) AddAIMessage(ctx context.Context, text string) error {
    return h.AddMessage(ctx, llms.AIChatMessage{Content: text})
}

// AddUserMessage 增加用户对话消息
func (h *ChatMessageHistory) AddUserMessage(ctx context.Context, text string) error {
    return h.AddMessage(ctx, llms.HumanChatMessage{Content: text})
}

// Clear 清空某个会话的全部对话历史
func (h *ChatMessageHistory) Clear(ctx context.Context) error {
    filter := bson.M{mongoSessionIDKey: h.sessionID}
    _, err := h.collection.DeleteMany(ctx, filter)
    return err
}

// AddMessage 新建一条对话记录
func (h *ChatMessageHistory) AddMessage(ctx context.Context, message llms.ChatMessage) error {
    _message, err := json.Marshal(llms.ConvertChatMessageToModel(message))
    if err != nil {
       return err
    }
    // 写入一条对话记录,使用sessionID区分对话记录属于哪一个会话
    _, err = h.collection.InsertOne(ctx, chatMessageModel{
       SessionID: h.sessionID,
       History:   string(_message),
    })

    return err
}

// SetMessages 覆盖某个会话的全部消息,实际上就是先清空这个会话的全部记录,然后再写入新的
func (h *ChatMessageHistory) SetMessages(ctx context.Context, messages []llms.ChatMessage) error {
    _messages := []interface{}{}
    for _, message := range messages {
       _message, err := json.Marshal(llms.ConvertChatMessageToModel(message))
       if err != nil {
          return err
       }
       _messages = append(_messages, chatMessageModel{
          SessionID: h.sessionID,
          History:   string(_message),
       })
    }

    if err := h.Clear(ctx); err != nil {
       return err
    }

    _, err := h.collection.InsertMany(ctx, _messages)
    return err
}

可以看到基于mongodb实现的对话记录存储整体逻辑也是很清晰的,每个对话记录对应一个文档,通过sessionID区分不同会话的记录,同时对文档的sessionID进行索引,以便于快速查找某个会话的对话记录。

以上已经介绍了llms、prompts以及memory的设计理念及源码,目前看起来还是比较琐碎的,没有将这几个模块联系起来,我接下来会再写一篇介绍tools、chains、agents以及callbacks的源码,chains和agents就会将整个流程全部串起来了。

相关推荐
PPIO派欧云2 小时前
PPIO × Lemon AI:一键解锁全流程自动化开发能力
人工智能·自动化·llm
强哥之神5 小时前
一文深入:AI 智能体系统架构设计
深度学习·语言模型·架构·llm·transformer·ai agent
semantist@语校5 小时前
如何为“地方升学导向型”语校建模?Prompt 框架下的宇都宫日建工科专门学校解析(7 / 500)
人工智能·百度·ai·语言模型·langchain·prompt·github
都叫我大帅哥8 小时前
LangChain加载HTML内容全攻略:从入门到精通
python·langchain
PPIO派欧云21 小时前
为什么主流大模型的上下文窗口都是128k?
llm
Paramita1 天前
LLM的技术底座:Transformer架构
llm
阿里云大数据AI技术1 天前
云上AI推理平台全掌握 (4):大模型分发加速
大数据·人工智能·llm
聚客AI1 天前
💡大模型开发从入门到部署:3个月避开87%的新手雷区
人工智能·pytorch·llm
weikuo05061 天前
【手搓大模型】从零手写Llama3
神经网络·llm