面试题:gpt长文本总结服务端实现

874 阅读3分钟

题目:长文本总结

我们有一个100,000字的文档(或者未知长度的文档),需要将其内容高度总结凝练至500字以内。

下面是一些假设:

  • 假设有一个大语言模型,我们需要通过API访问。处理每个API的延迟时间在15-30秒之间,每个API调用可以处理的文本字数有限(最大4000字)。
  • 每次API调用通常可以将4000字(按最大窗口计算)压缩总结到300-700字之间。
  • 大语言模型设有处理速度峰值限制。单一API客户每分钟可以处理的最大字数为40,000字。达到峰值后需要等待。
  • 需要大模型的API调用有大约5%会碰到服务器忙的出错率。
  • 加分考虑:我们有一个API客户pool有大约50个API客户(可以认为大模型的最高访问率是40,000x50字/分钟)。我们如何考虑用此资源来最大限度的服务于我们的终端客户,达到高速、高可靠性的效果。
  • 加分考虑:如果在我们的终端客户中设立VIP客户和普通客户,在资源和带宽有限的情况下,希望有限保证VIP客户的服务质量(尽量小的延迟)。

请你为给定的输入文档设计一个方案,使得我们可以在最短的延迟时间内获得一个500字以内的总结。同时,你需要注意优化成本。你可以使用Python,Node.js,Golang,Java等语言来提供半伪代码实现。如果需要,你可以做出合理的假设。

请你对你的解决方案进行总结和分析,说明需要注意的问题以及可以考虑的改进方法。对于较复杂的实现,如果无法在pseodu code中体现,可以在解决方案分析中阐明。


import (
   "fmt"
   "strings"
   "sync"
   "time"
)

var ProcessChan = make(chan *message, 1024) //全局channel, 用于接收生产者
var VipHeap = MaxHeap{}                     // vip等级最大堆
var vipLock = sync.Mutex{}

func main() {
   apiKeys := []string{"111", "222", "333"} // 有一个API客户pool有大约50个API客户, 这里3个代表50个,简化
   // 创建50个消费者
   for i := 0; i < len(apiKeys); i++ {
   	apiKey := apiKeys[i]
   	go func() {
   		consumer(apiKey)
   	}()
   }
   // 监听客户端请求
   for true {
   	userId, text := httpListenClient()
   	// 异步增加并发量,这里高级实现其实可以用epoll,io多路复用
   	go func() {
   		result := producer(text, userId)
   		fmt.Println(result)
   	}()

   }

}

type message struct {
   userId     int64
   userLevel  int64
   index      int
   text       string
   resultChan chan gptResult
}

type gptResult struct {
   index int
   text  string
}

/*
1. 接受客户端请求
2。 请求拆分成4000字的小包,投递给消费者
3。 等待消费者返回,组装数据
*/
func producer(text string, userId int64) string { //有一个100,000字的文档
   res := text
   var chunks []string
   var results []string
   var resultChan = make(chan gptResult, 1024)
   for !(len(res) < 700 && len(res) > 300) { //压缩总结到300-700字之间。
   	chunks = split(res, 4000) // 每次API调用通常可以将4000字(按最大窗口计算)
   	results = make([]string, len(chunks))
   	for i := 0; i < len(chunks); i++ {
   		chunk := chunks[i]
   		go func() {
   			m := &message{
   				userId:     userId,
   				userLevel:  vipLevel(userId),
   				index:      i,
   				text:       chunk,
   				resultChan: resultChan,
   			}
   			vipLock.Lock()
   			VipHeap.Push(m)
   			vipLock.Unlock()
   		}()
   	}
   	for i := 0; i < len(results); i++ {
   		gptRes := <-resultChan
   		results[gptRes.index] = gptRes.text
   	}
   	res = strings.Join(results[:], ". ")
   }
   return res
}

/*
1。 接收生产者投递的请求
2。 发起http调用gpt
3。 将结果返回生产者
*/
func consumer(apiKey string) {
   go func() {
   	for true {
   		vipLock.Lock()
   		if VipHeap.Len() > 0 {
   			m := VipHeap.Pop().(*message)
   			ProcessChan <- m
   		}
   		vipLock.Unlock()
   	}
   }()

   lock := sync.Mutex{}
   ticker := time.NewTicker(time.Second * 60) //每分钟可以处理的最大字数为40,000字。
   tokenBucket := 40000
   // 异步每分钟更新令牌桶
   go func() {
   	select {
   	case <-ticker.C:
   		{
   			lock.Lock()
   			tokenBucket = 40000
   			lock.Unlock()
   		}
   	}
   }()
   for true {
   	select {
   	// 监听全局 ProcessChan
   	case m := <-ProcessChan:
   		text := m.text
   		if len(text) > tokenBucket {
   			time.Sleep(3 * time.Second) // 这里这个时间可以优化
   			ProcessChan <- m
   			continue
   		}
   		res, err := httpGet(apiKey, text)
   		if err != nil {
   			//失败的时候,等待一段时间再去请求 (大模型的API调用有大约5%会碰到服务器忙的出错率)
   			time.Sleep(1 * time.Second)
   			ProcessChan <- m
   			continue
   		}
   		m.resultChan <- gptResult{
   			index: m.index,
   			text:  res,
   		}
   		lock.Lock()
   		tokenBucket = tokenBucket - len(res)
   		lock.Unlock()

   	}

   }

}

// 伪api请求
func httpGet(apiKey string, text string) (string, error) {
   return "chatGpt response", nil
}

// 伪api请求
func httpListenClient() (userId int64, text string) {
   return 0, "text"
}

// 伪vip等级赋权函数
func vipLevel(userId int64) int64 {
   return 0
}

// MaxHeap 实现用户id的最大堆
type MaxHeap []*message

func (h MaxHeap) Less(i, j int) bool  { return h[i].userLevel > h[j].userLevel }
func (h MaxHeap) Swap(i, j int)       { h[i], h[j] = h[j], h[i] }
func (h MaxHeap) Len() int            { return len(h) }
func (h *MaxHeap) Push(x interface{}) { *h = append(*h, x.(*message)) }
func (h *MaxHeap) Pop() interface{} {
   n := len(*h)
   ans := (*h)[n-1]
   *h = (*h)[:n-1]
   return ans
}

func split(s string, chunkSize int) []string {
   if len(s) == 0 {
   	return nil
   }
   if chunkSize >= len(s) {
   	return []string{s}
   }
   var chunks = make([]string, 0, (len(s)-1)/chunkSize+1)
   currentLen := 0
   currentStart := 0
   for i := range s {
   	if currentLen == chunkSize {
   		chunks = append(chunks, s[currentStart:i])
   		currentLen = 0
   		currentStart = i
   	}
   	currentLen++
   }
   chunks = append(chunks, s[currentStart:])
   return chunks
}