Kimi API 助手的氮气加速装置 —— 以 Golang 为例实践 Context Caching 2

进击的黑咻,7 min read

上一个章节中,我们以 Golang 为例实践了使用 Context Caching 为 Kimi API 助手(简称小助手)提速,让我们简单地回顾一下主要内容:

  1. 小助手会将所有知识以 system prompt 的形式放置在上下文(Context)中(即 initKnowledge 函数);
  2. 我们使用 Context Caching 功能,缓存小助手的 system prompt;
  3. 我们新增了一个缓存管理模块(以 sqlite 为例),使用业务的 Key 与缓存 cache_id 进行映射,以解决 cache_id 变动的问题;
  4. 改造 initKnowledge 函数,加入查询和新增缓存功能;
  5. 改造 CreateChatCompletion 接口,通过设置 HTTP Headers 以使用缓存;

在实际使用缓存的过程中,我们也陆续收到很多小伙伴的意见和建议,其中最频繁被提到的问题是,使用 HTTP Headers 的方式使用缓存时,仍然需要在 messages 列表中附上完整的缓存 messages,既增加了网络传输的开销,又容易出现因错误操作缓存 messages 导致缓存无法命中的问题,甚至有同学提出,使用 HTTP Headers 的方式调用缓存,本身就非常不直观,在某些场合下并不能“无痛”改造原有业务逻辑,不可避免地会产生 Breaking Change。

因此,我们听取和吸纳了一些小伙伴提出的建议,为 Context Caching 添加了一个语法糖,现在你可以在 messages 列表中的第一位,使用一条 role="cache" 的 message 来引用已被缓存的内容,这条 message 的样子看起来是这样:

{
    "role": "cache",
    "content": "cache_id=cache-xxxxxxxxxxxxxxxxxxxxx;reset_ttl=3600"
}

使用这样的 message 时,你需要遵循以下几个约定:

我们会通过实际的例子来讲解如何使用这一项语法糖,并且看看这个语法糖有多甜。

通过设置 Message 的方式调用缓存

我们仍然使用上一章节所使用的 initKnowledge 函数,以及缓存管理模块 ContextCacheManager,我们将重点放在如何快速、正确地构造一个 role="cache" 的 message,并且尽最大可能不产生 Breaking Change。

改造 Content 结构体

细心的小伙伴们应该会发现,在上一篇中,我们在构造 Message 结构体时,使用的 Content 字段并不是 string 类型,而是使用了一个自定义结构体 Content,我们先来看看这个结构体的布局:

type Content struct {
	Text  string
}
 
func (c *Content) MarshalJSON() ([]byte, error) {
	if c == nil {
		return json.Marshal(nil)
	}
	return json.Marshal(c.Text)
}

注:使用 Content 结构体而非 string 类型,是因为我们预期 message.content 的值会有不同的类型,因此预留了这样的结构体以便于后续迭代,目前看来,确实派上用场了。

为了兼容 role="cache" 的 message,我们会给 Content 结构体新增一个字段 Cache,并改写 MarshalJSON 方法使其能支持特性的 Cache 语法,改造后的 Content 看起来像这样:

// ContextCacheOptions 是在上一个章节出现过的老朋友了
type ContextCacheOptions struct {
	CacheID  string
	ResetTTL int
}
 
type Content struct {
	Text  string
	Cache *ContextCacheOptions
}
 
func (c *Content) MarshalJSON() ([]byte, error) {
	if c == nil {
		return json.Marshal(nil)
	}
	if c.Cache != nil {
		var cacheOptionsBuilder strings.Builder
		cacheOptionsBuilder.WriteString("cache_id=")
		cacheOptionsBuilder.WriteString(c.Cache.CacheID)
		if resetTTL := c.Cache.ResetTTL; resetTTL > 0 {
			cacheOptionsBuilder.WriteString(";")
			cacheOptionsBuilder.WriteString("reset_ttl=")
			cacheOptionsBuilder.WriteString(strconv.Itoa(resetTTL))
		}
		return json.Marshal(cacheOptionsBuilder.String())
	}
	return json.Marshal(c.Text)
}

当使用 Content 结构体时,我们可以任意选择赋值 Text 字段或 Cache 字段,二者选其一,当使用 Text 字段时,它将被序列化为一个普通的 string 类型的值;而当使用 Cache 字段时,它将按照约定的格式,将 Cache 相关信息(包括 cache_idreset_ttl)转换为特定的格式,并生成一个满足要求的 string 类型的值。

使用 Content 结构体启用缓存

我们仍然会使用 initKnowledge 函数和 ContextCacheManager 接口,我们新建一个函数名为 ChatWithCacheByMessage

func chatWithCacheByMessage(
	ctx context.Context,
	client Client[moonshot],
	cacheManager ContextCacheManager,
) {
	messages, cacheID, err := initKnowledge(ctx, client, cacheManager)
	if err != nil {
		if parsed := ParseError(err); parsed != nil {
			log.Fatalln("("+parsed.Type+")", parsed.Message)
		}
		log.Fatalln(err)
	}
 
	// 当缓存存在时,我们直接使用通过 Message 引用缓存内容,通过设置 Message.Content.Cache
	// 值的方式传递 cache_id 和 reset_ttl 参数,以此在调用 CreateChatCompletion 接口时开
	// 启缓存。
	//
	// 当缓存不存在时,我们需要从 ContextCacheManager 中取回 Cache 原本的 messages 信息,
	// 然后以不启用缓存的形式调用 CreateChatCompletion 接口。
 
	if cacheID != "" {
		messages = []*Message{
			{Role: RoleCache, Content: &Content{Cache: &ContextCacheOptions{CacheID: cacheID, ResetTTL: 3600}}},
			{Role: RoleUser, Content: &Content{Text: input}},
		}
	} else {
		messages = append(messages, &Message{
			Role:    RoleUser,
			Content: &Content{Text: input},
		})
	}
 
	stream, err := client.CreateChatCompletionStream(ctx, &ChatCompletionStreamRequest{
		Messages: messages,
		Model:    ModelMoonshot128K,
	})
 
	if err != nil {
		if parsed := ParseError(err); parsed != nil {
			log.Fatalln("("+parsed.Type+")", parsed.Message)
		}
		log.Fatalln(err)
	}
 
	defer stream.Close()
	for chunk := range stream.C {
		fmt.Printf("%s", chunk.GetDeltaContent())
	}
}

注:原 chat 函数被重命名为 ChatWithCacheByContext

通过改造 Message.Content,我们实现了引用已经创建成功的缓存,在减少网络传输开销的同时,我们还避免了代码的 Breaking Change,只需要在生成 messages 列表时,额外在列表首尾置入一个特殊的 message(即 role="cache" 的 message)即可完成缓存启用操作,同时,在配置缓存时,我们还使用了结构体来规范缓存配置项,做到代码有补全,阅读能跳转,编译器能检查三手都要抓,三手都要硬。

相关代码及代码示例中所涉及的 SDK 均可在我们的 Github (opens in a new tab) 中获取。

2024 © Moonshot AI用户中心文档