链式调用与 Prompt 模板

"链"(Chain)是 LangChain 的核心抽象:将模板填充、模型调用、输出解析等步骤组合为可复用的流程。langchaingo 通过 promptschains 包实现这一模型。

PromptTemplate

模板使用 Go 的 text/template 语法,声明时需列出全部变量:

package main

import (
    "context"
    "fmt"

    "github.com/tmc/langchaingo/chains"
    "github.com/tmc/langchaingo/llms/openai"
    "github.com/tmc/langchaingo/prompts"
)

func main() {
    llm, err := openai.New()
    if err != nil {
        panic(err)
    }

    prompt := prompts.NewPromptTemplate(
        "你是一名资深的 {{.language}} 代码评审员。请评审以下代码,指出主要问题:\n{{.code}}",
        []string{"language", "code"}, // 模板变量清单
    )

    llmChain := chains.NewLLMChain(llm, prompt)

    ctx := context.Background()
    // 单输入链可用 Run;多输入链必须使用 Call
    out, err := chains.Call(ctx, llmChain, map[string]any{
        "language": "Go",
        "code":     "func add(a, b int) int { return a + b }",
    })
    if err != nil {
        panic(err)
    }

    fmt.Println(out[llmChain.OutputKey])
}
Tip

chains.Run(ctx, chain, input) 是单输入链的快捷方式,等价于 chains.Call 传入只有一个键的映射;返回值直接是字符串。多变量链一律使用 Call,结果通过 llmChain.OutputKey 取出。

顺序链:组合多个步骤

复杂任务可以拆分为多个链串联:前一步的输出作为后一步的输入。典型流程是"先让模型生成,再让模型评估":

package main

import (
    "context"
    "fmt"

    "github.com/tmc/langchaingo/chains"
    "github.com/tmc/langchaingo/llms/openai"
    "github.com/tmc/langchaingo/prompts"
)

func main() {
    llm, _ := openai.New()
    ctx := context.Background()

    // 第一步:生成标题,输出键默认为 "text"
    generatePrompt := prompts.NewPromptTemplate(
        "为关于 {{.topic}} 的技术博客提出三个标题。",
        []string{"topic"},
    )
    generateChain := chains.NewLLMChain(llm, generatePrompt)

    // 第二步:基于第一步的输出(text)挑选标题
    reviewPrompt := prompts.NewPromptTemplate(
        "以下标题列表中,请选择最适合初学者的一条并说明理由:\n{{.text}}",
        []string{"text"},
    )
    reviewChain := chains.NewLLMChain(llm, reviewPrompt)
    reviewChain.OutputKey = "summary" // 输出键不得与上游冲突

    // 顺序链:整体输入 topic,最终输出 summary
    seqChain, err := chains.NewSequentialChain(
        []chains.Chain{generateChain, reviewChain},
        []string{"topic"},
        []string{"summary"},
    )
    if err != nil {
        panic(err)
    }

    out, err := chains.Call(ctx, seqChain, map[string]any{"topic": "Go 并发编程"})
    if err != nil {
        panic(err)
    }
    fmt.Println(out["summary"])
}
Tip

顺序链的键必须严格衔接:每条链的输入变量要么来自整体输入,要么来自上游某条链的输出键;各链输出键不能重复。默认输出键为 text,需要区分时自定义 OutputKey

会话记忆

NewConversation 内置了对话模板(history + input)与记忆接口,几行代码即可获得有上下文的多轮对话:

package main

import (
    "context"
    "fmt"

    "github.com/tmc/langchaingo/chains"
    "github.com/tmc/langchaingo/llms/openai"
    "github.com/tmc/langchaingo/memory"
)

func main() {
    llm, _ := openai.New()
    ctx := context.Background()

    conversation := chains.NewConversation(llm, memory.NewConversationBuffer())

    _, _ = chains.Call(ctx, conversation, map[string]any{"input": "我叫小明"})
    out, _ := chains.Call(ctx, conversation, map[string]any{"input": "我叫什么名字?"})
    fmt.Println(out["text"]) // 模型能结合历史回答:小明
}
Tip

记忆策略选择:ConversationBuffer 原样保存全部历史,简单但会随轮数膨胀;长会话应改用长度受限的缓冲记忆(NewConversationTokenBuffer)或将历史落库,通过 database 自行管理。

输出结构化

模型输出常需转为结构体。先约束模型输出 JSON,再配合 encoding/json 解析:

type Review struct {
    Score   int    `json:"score"`
    Summary string `json:"summary"`
}

// Prompt 中明确要求 JSON 输出
prompt := prompts.NewPromptTemplate(
    "评审以下 Go 代码,以 JSON 输出,字段为 score(1-10) 与 summary:\n{{.code}}",
    []string{"code"},
)

// 模型返回后:
var review Review
if err := json.Unmarshal([]byte(answer), &review); err != nil {
    // 输出可能携带 markdown 代码围栏,需先剥离再解析
}
Warning

LLM 输出具有不确定性,直接 json.Unmarshal 可能因代码围栏(```json)或多余文本失败。健壮的做法是:截取第一个 { 到最后一个 } 之间的内容,或使用支持结构化输出/JSON 模式的模型参数。

与并发结合

链本身不是并发安全的共享对象,但可以并发地调用同一条链,用 WaitGroup 批量处理任务:

items := []string{"订单A", "订单B", "订单C"}
results := make([]string, len(items))
var wg sync.WaitGroup
for i, item := range items {
    wg.Add(1)
    go func(idx int, text string) {
        defer wg.Done()
        out, err := chains.Call(ctx, llmChain, map[string]any{"text": text})
        if err == nil {
            results[idx] = out["text"].(string)
        }
    }(i, item)
}
wg.Wait()

小结

  • prompts.NewPromptTemplate + chains.NewLLMChain 构成最小编排单元。
  • 多输入链用 chains.Call,单输入链可用 chains.Run;复杂流程用顺序链拆解。
  • 记忆组件维护对话历史;结构化输出需在 Prompt 中约束并做容错解析。