手记

适合于go语言开发者的AIGC开发技巧-理解Chain使用并实现RouterChain

2024-10-28 15:47:14199浏览

木兮QwQ

2实战 · 3手记

langchainGo是目前Go语言中应用于AIGC的框架基于Python版本的langchain所设计,但是在功能上有较多的不足。其中就如RouterChain在langchainGo中并没有实现,因此如果我们想要使用就只能自己去从0实现一个RouterChain。

理解langchain的基础调度运行

对于langchain在业界中的介绍大多是描述它有6个核心模块:

  • prompts: 提示词模块,用于处理模型中的提示词信息 可以 与outputparser配合起来;outputparser是用于约束并解析大模型返回的数据内容
  • model:模型驱动:langchain所有功能都是围绕它而展开,是底层驱动。我们可以自定义扩展对其他模型的支持,如支持通义千问。
  • chains:链:正常在代码编写的时候prompts、memory、model等在使用的时候是依次编写然后调度执行的。而用了chains可以将其组合在一起执行,当然除了当前描述的执行封装以外,还可以组合多种任务chains也是可以的。
  • agent:代理:我们会提供一组工具列表,然后输入一个信息。langchain就可以通过agent基于输入信息模拟人思考该用那个工具执行,执行完后思考是结束或继续下一个工具
  • memory:记忆:正常openai接口是不存在记忆功能的,因此记忆的本质就是将之前对话的信息与当前用户输入的提示词组合发送给大模型,而memory则就是实现了该功能的一组策略。
  • index:索引:与向量配合,常用于实现本地知识库;通过langchain提供的文件读取切分的方式将信息发送给大模型生成向量数据并存储,在使用的时候也会将用户问题转化为向量值本地去进行向量匹配以查找用户所需。

model与prompts相对简单,如示例:

package test

import (
    "context"
    "fmt"
    "log"
    "testing"

    "github.com/tmc/langchaingo/llms"
    "github.com/tmc/langchaingo/llms/openai"
    "github.com/tmc/langchaingo/memory"
    "github.com/tmc/langchaingo/outputparser"
    "github.com/tmc/langchaingo/prompts"
)

var (
    url = ""
    apiKey = ""
)

func getLLmOpenaiClient(t *testing.T, opts ...openai.Option) *openai.LLM {
    opts = append(opts, openai.WithBaseURL(url), openai.WithToken(apiKey))
    llm, err := openai.New(opts...)
    if err != nil {
    t.Fatal(err)
    }
    return llm
}

func TestLLM_Introduce_OutPut(t *testing.T) {
    ctx := context.Background()

    var (
        // 定义提示词模板,用户输入的信息关键信息只有 dep: 部门、 name: 用户名
        template         = "请你为{{.dep}}部门新入职的员工{{.name}},设计一个自我介绍; 要求中文介绍;"
        // 定义提示词模板参数:根据提示词模板使用 {{. xx}} 方式声明的都属于提示词参数
        templateInputVal = []string{"dep", "name"}
        // 假设输入信息
        // key 需要与 templateInputVal 一致不能少也不能错,不然会报错,可以多
        staff := map[string]any{
            "name": "lili",
            "dep":  "go开发",
        }
    )

    // 定义outputparser,当前采用结构化格式;
    // 此处声明有两处用途:
    //      1. 用于告诉大模型我期望的数据结构
    //      2. 从大模型返回的字符串信息中解析出我想要的数据
    output := outputparser.NewStructured([]outputparser.ResponseSchema{
        { // 字段 content 以及描述
            Name:        "content",
            Description: "介绍内容",
        }, { // 字段 reason 以及描述
            Name:        "reason",
            Description: "为什么这么介绍",
        },
    })
    // 根据传递的字段需求生成提示词模板,该模板就是用于邀请大模型返回的数据结构
    // 模板示例:The output should be a markdown code snippet formatted in the following schema:
    // ```json
    // {
    //      "content": // 介绍内容,
    //      "reason": // 为什么这么介绍
    // }
    // ```
    instructinons := output.GetFormatInstructions()

    // 创建提示词处理对象 prompts ;template+"\n"+instructinons 是最终提示词模板
    prompt := prompts.NewPromptTemplate(template+"\n"+instructinons, templateInputVal)

    // prompt根据提示词模板 用户 传参,然后组合生成最终提示词信息;如下
    //
    // 请你为 go开发 部门新入职的员工 lili ,设计一个自我介绍; 要求中文介绍;
    // The output should be a markdown code snippet formatted in the following schema:
    // ```json
    // {
    //      "content": // 介绍内容,
    //      "reason": // 为什么这么介绍
    // }
    // ```
    v, err := prompt.FormatPrompt(staff)
    NoError(t, err)

    // 创建模型对象
    llm := getLLmOpenaiClient(t)

    // 请求大模型获取
    text, err := llms.GenerateFromSinglePrompt(ctx, llm, v.String())
    NoError(t, err)

    // 调用outputparser中的parser解析大模型返回的结果text
    // outputparser 根据前面定义的字段列表去解析,最终输出如下
    //
    // map[string]string{
    //     "content": "xxx",
    //     "reason": "xxx",
    // }
    data, err := output.Parse(text)
    NoError(t, err)

    t.Log(data)
}

func NoError(t *testing.T, err error) {
    if err != nil {
        t.Fatal(err)
    }
}

在上面的例子中我们就运用了model、prompts、outputparser这三个功能;从流程中先定义好属性参数、然后创建好outputparser、prompts属性并使用这二者生成最终的提示词。

将生成的提示词传递到大模型中发起请求,在模型执行成功后针对输出的结果进行解析并返回出来,这样如果我们在业务中使用也可以获取到相应的结果信息。

理解chains的使用

“正常在代码编写的时候prompts、memory、model等在使用的时候是依次编写然后调度执行的。而用了chains可以将其组合在一起执行,当然除了当前描述的执行封装以外,还可以组合多种任务chains也是可以的。”

如上是我们对chains最开始的介绍,我们可以用chain将组件功能组合在一起运行 也可以 将多个chains组合在一起实现一组复杂的业务。

那么首先我们来看在使用chains的时候如何使用:如示例

func TestLLM_chains(t *testing.T) {

    ctx := context.Background()

    var (
        // 定义提示词模板,用户输入的信息关键信息只有 dep: 部门、 name: 用户名
        template         = "请你为{{.dep}}部门新入职的员工{{.name}},设计一个自我介绍; 要求中文介绍;"
        // 定义提示词模板参数:根据提示词模板使用 {{. xx}} 方式声明的都属于提示词参数
        templateInputVal = []string{"dep", "name"}
    )

    // 定义outputparser,当前采用结构化格式;
    // 此处声明有两处用途:
    //      1. 用于告诉大模型我期望的数据结构
    //      2. 从大模型返回的字符串信息中解析出我想要的数据
    output := outputparser.NewStructured([]outputparser.ResponseSchema{
        { // 字段 content 以及描述
            Name:        "content",
            Description: "介绍内容",
        }, { // 字段 reason 以及描述
            Name:        "reason",
            Description: "为什么这么介绍",
        },
    })
    // 根据传递的字段需求生成提示词模板,该模板就是用于邀请大模型返回的数据结构
    // 模板示例:The output should be a markdown code snippet formatted in the following schema:
    // ```json
    // {
    //      "content": // 介绍内容,
    //      "reason": // 为什么这么介绍
    // }
    // ```
    instructinons := output.GetFormatInstructions()

    // 创建提示词处理对象 prompts ;template+"\n"+instructinons 是最终提示词模板
    prompt := prompts.NewPromptTemplate(template+"\n"+instructinons, templateInputVal)

    // 创建模型对象
    llm := getLLmOpenaiClient(t)

    // 创建chains
    c := chains.NewLLMChain(llm, prompt)
    c.OutputParser = output

    // 基于chains组合上面的功能执行并输出最终结果
    res, err := chains.Call(ctx, c, map[string]any{
        "name": "lili",
        "dep":  "go开发",
    })
    NoError(t, err)

    t.Log(res)
}

如果你希望多个列组合,那么可以参考伪代码

// 方式1
chains := []chains.Chain{
    chains.NewLLMChain(llm, prompt1),
    chains.NewLLMChain(llm, prompt2),
}
simpleSeqChain, err := chains.NewSimpleSequentialChain(chains)
NoError(t, err)
res, err := chains.Call(context.Background(), simpleSeqChain, map[string]any{
    "input": "What did the chicken do?",
})

// 方式2
type MyChains struct {
    c chains.Chain
}
func (m *MyChains) Call(ctx context.Context, inputs map[string]any, options ...ChainCallOption) (map[string]any, error) {
    // 业务
    res, err := chains.Call(ctx, m.c, inputs, options...)
    // 业务
    return res, err
}
func (m *MyChains) GetMemory() schema.Memory {
    return memory.NewSimple()
}
func (m *MyChains) GetInputKeys() []string {
    return []string{}
}
func (m *MyChains) GetOutputKeys() []string {
    return []string{}
}
func TestMyChains(t *Testing.T) {
    myChains := &MyChains{
        c:chains.NewLLMChain(llm, prompt1)
    }
    res, err := chains.Call(context.Background(), myChains, map[string]any{
        "input": "What did the chicken do?",
    })
}

手写RouterChains

RouterChain的功能就是分发,根据用户的输入信息然后选择合适的目标进行执行,这个功能在大模型的实践应用中还是比较重要的,用户从AIChat的输入界面输入信息,如果要定制个性化的业务就需要先根据用户的问题选择合适的处理对象。但是在langchainGo中暂时还没有把这个功能增加上来。

对于该功能的实现在本质思路上就是

  1. 用户输入
  2. 根据用户输入选择合适的目标
  3. 然后调用模板处理任务

如下是RouterChain的核心提示词,这个提示词的来源是Python版本的langchain中的提示词信息

// Prompt for the router chain in the multi-prompt chain.
const (
    _destinations = "destinations"
    _input        = "input"
    _text         = "text"
    _formatting   = "formatting"

    MULTI_PROMPT_ROUTER_TEMPLATE = `Given a raw text input to a language model select the model prompt best suited for
the input. You will be given the names of the available prompts and a description of
what the prompt is best suited for. You may also revise the original input if you
think that revising it will ultimately lead to a better response from the language
model.

<< FORMATTING >>
Return a markdown code snippet with a JSON object formatted to look like:
{{.formatting}}

REMEMBER: "destination" MUST be one of the candidate prompt names specified below OR \
it can be "DEFAULT" if the input is not well suited for any of the candidate prompts.
REMEMBER: "next_inputs" can just be the original input if you don't think any \
modifications are needed.

<< CANDIDATE PROMPTS >>
{{.destinations}}

<< INPUT >>
{{.input}}
`
)

var (
    _prompt = prompts.NewPromptTemplate(MULTI_PROMPT_ROUTER_TEMPLATE, []string{_destinations,_input, _formatting})
    _outputParser = outputparser.NewStructured([]outputparser.ResponseSchema{
        {
            Name:        _destinations,
            Description: "name of the prompt to use or \"DEFAULT\"",
        },
        {
            Name:        _input,
            Description: "a potentially modified version of the original input",
        },
    })
)

然后就是对RouterChain的核心实现,直接上代码


package routerchain

import (
    "context"
    "github.com/tmc/langchaingo/chains"
    "github.com/tmc/langchaingo/llms"
    "github.com/tmc/langchaingo/memory"
    "github.com/tmc/langchaingo/outputparser"
    "github.com/tmc/langchaingo/prompts"
    "github.com/tmc/langchaingo/schema"
)

type Router struct {
    Destination string
    Key         string
    Prompt      prompts.FormatPrompter
    Chains      chains.Chain
}

type MultiPromptInfo struct {
    Key         string
    Destination string
    Prompt      prompts.FormatPrompter
}

// 多提示词路由
type MultiChains struct {
    LLMChain     *chains.LLMChain
    Routers      map[string]*Router
    Destinations map[string]string
    OutputParser outputparser.Structured
}

func NewMultiChains(llm llms.Model, multiPrompts []*MultiPromptInfo) *MultiChains {
    var (
        routerLlMChain = make(map[string]*Router, len(multiPrompts))
        destination    = make(map[string]string, len(multiPrompts))
    )

    for i, _ := range multiPrompts {
        routerLlMChain[multiPrompts[i].Key] = &Router{
            Chains:      chains.NewLLMChain(llm, multiPrompts[i].Prompt),
            Destination: multiPrompts[i].Destination,
            Key:         multiPrompts[i].Key,
            Prompt:      multiPrompts[i].Prompt,
        }
        destination[multiPrompts[i].Key] = multiPrompts[i].Destination
    }

    return &MultiChains{
        Destinations: destination,
        Routers:      routerLlMChain,
        LLMChain:     chains.NewLLMChain(llm, _prompt),
        OutputParser: _outputParser,
    }
}

func (c *MultiChains) Call(ctx context.Context, inputs map[string]any,
options ...chains.ChainCallOption) (map[string]any, error) {
    // 获取用户输入
    destinations := inputs[_input]
    // 调用大模型根据用户输入选择执行对象
    result, err := chains.Call(ctx, c.LLMChain, map[string]any{
        _input:        destinations,
        _destinations: c.Destinations,
        _formatting:   c.OutputParser.GetFormatInstructions(),
    }, options...)
    if err != nil {
        return nil, err
    }

    text, ok := result[_text]
    if !ok {
        return nil, chains.ErrNotFound
    }
    // 根据确定的目标执行
    return c.processLLMResult(ctx, text.(string), inputs)
}

// 解析要执行的目标对象,并执行目标
func (c *MultiChains) processLLMResult(ctx context.Context, text string,
inputs map[string]interface{}) (map[string]interface{},
error) {
    data, err := c.OutputParser.Parse(text)
    if err != nil {
        return nil, err
    }
    next := data.(map[string]string)
    router := c.Routers[next[_destinations]].Chains

    return chains.Call(ctx, router, inputs)
}

// GetMemory gets the memory of the chain.
func (c *MultiChains) GetMemory() schema.Memory {
    return memory.NewSimple()
}

// GetInputKeys returns the input keys the chain expects.
func (c *MultiChains) GetInputKeys() []string {
    return nil
}

// GetOutputKeys returns the output keys the chain returns.
func (c *MultiChains) GetOutputKeys() []string {
    return nil
}

如下就是测试的具体用例:

package routerchain

import (
    "context"
    "github.com/tmc/langchaingo/chains"
    "github.com/tmc/langchaingo/prompts"
    "testing"

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

var (
    apiKey = ""
    url    = ""
)

func getLLmOpenaiClient(t *testing.T, opts ...openai.Option) *openai.LLM {
    opts = append(opts, openai.WithBaseURL(url), openai.WithToken(apiKey))
    llm, err := openai.New(opts...)
    if err != nil {
        t.Fatal(err)
    }
    return llm
}

func NoError(t *testing.T, err error) {
    if err != nil {
        t.Fatal(err)
    }
}

func Test_MultiText(t *testing.T) {
    multis := []*MultiPromptInfo{
        {
            Key:         "basketball",
            Destination: "适合回答关于打篮球的问题",
            Prompt: prompts.NewPromptTemplate("你是一个经验丰富的篮球教练,擅长解答关于篮球的问题。 下面是需要你来回答的问题: {{.input}}",
            []string{"input"}),
        }, {
            Key:         "swimming",
            Destination: "适合回答关于游泳的问题",
            Prompt: prompts.NewPromptTemplate("你是一位多年经验的游泳教练,擅长解答关于游泳的问题。 下面是需要你来回答的问题:{{.input}}",
            []string{"input"}),
        },
    }

    llm := getLLmOpenaiClient(t)

    chain := NewMultiChains(llm, multis)

    res, err := chains.Call(context.Background(), chain, map[string]any{
        "input": "请问蛙泳和蝶泳的区别是什么",
    })

    t.Log(res)
    t.Log(err)
}
0人推荐
随时随地看视频
慕课网APP