package main import ( "bufio" "bytes" "context" "encoding/json" "fmt" "github.com/volcengine/volc-sdk-golang/base" "io/ioutil" "net/http" "net/url" "strings" "time" ) const ( SysFieldDocName = "doc_name" SysFieldTitle = "title" SysFieldChunkTitle = "chunk_title" SysFieldContent = "content" ) /* 拼接Prompt时用户需要传入的字段 如果想获得较好的效果,建议: 如果知识库中的数据为非结构化数据,SystemFields 中可以传入 doc_name, title, chunk_title, content等字段,其中 content 字段为必传字段,其他为可选字段 如果知识库中的数据为结构化数据,SystemFields 中可以传入title字段(可选),SelfDefineFields字段来源于为结构化数据的表头字段,表头字段中索引字段需要必传,非索引字段可以不传 */ type PromptExtraContext struct { SelfDefineFields []string `json:"self_define_fields"` SystemFields []string `json:"system_fields"` } /* 检索接口请求参数 */ type CollectionSearchKnowledgeRequest struct { Name string `json:"name,omitempty"` Project string `json:"project,omitempty"` ResourceId string `json:"resource_id,omitempty"` Query string `json:"query"` Limit int32 `json:"limit"` QueryParam *QueryParamInfo `json:"query_param"` DenseWeight float32 `json:"dense_weight"` MdSearch bool `json:"md_search"` Preprocessing PreProcessing `json:"pre_processing,omitempty"` Postprocessing PostProcessing `json:"post_processing,omitempty"` } type QueryParamInfo struct { DocFilter interface{} `json:"doc_filter"` } type MessageParam struct { Role string `json:"role"` Content interface{} `json:"content"` } type ChatCompletionMessageContent struct { StringValue *string ListValue []*ChatCompletionMessageContentPart } type ChatMessageImageURL struct { URL string `json:"url,omitempty"` } type ChatCompletionMessageContentPartType string const ( ChatCompletionMessageContentPartTypeText ChatCompletionMessageContentPartType = "text" ChatCompletionMessageContentPartTypeImageURL ChatCompletionMessageContentPartType = "image_url" ) type ChatCompletionMessageContentPart struct { Type ChatCompletionMessageContentPartType `json:"type,omitempty"` Text string `json:"text,omitempty"` ImageURL *ChatMessageImageURL `json:"image_url,omitempty"` } /* 检索接口预处理参数Part,详细介绍见官方文档 */ type PreProcessing struct { NeedInstruction bool `json:"need_instruction"` Rewrite bool `json:"rewrite"` Messages []MessageParam `json:"messages"` ReturnTokenUsage bool `json:"return_token_usage"` } /* 检索接口后处理参数Part,详细介绍见官方文档 */ type PostProcessing struct { RerankSwitch bool `json:"rerank_switch"` RerankModel string `json:"rerank_model,omitempty"` RerankOnlyChunk bool `json:"rerank_only_chunk"` RetrieveCount int32 `json:"retrieve_count"` EndpointID string `json:"endpoint_id"` ChunkDiffusionCount int32 `json:"chunk_diffusion_count"` ChunkGroup bool `json:"chunk_group"` ChunkScoreAggrType string `json:"chunk_score_aggr_type,omitempty"` ChunkExtraContent map[string]interface{} `json:"chunk_extra_content"` GetAttachmentLink bool `json:"get_attachment_link"` } /* 检索接口返回参数结构体,详细介绍见官方文档 */ type CollectionSearchKnowledgeResponse struct { Code int64 `json:"code"` Message string `json:"message,omitempty"` Data *CollectionSearchKnowledgeResponseData `json:"data,omitempty"` } type CollectionSearchKnowledgeResponseData struct { CollectionName string `json:"collection_name"` Count int32 `json:"count"` RewriteQuery string `json:"rewrite_query,omitempty"` // 改写后的query,如果是多轮对话的首次请求,该字段为空,表示不改写,从第二个问题开始进行改写 TokenUsage *TotalTokenUsage `json:"token_usage,omitempty"` ResultList []*CollectionSearchResponseItem `json:"result_list,omitempty"` } /* 检索接口各个阶段模型调用量详情,详细介绍见官方文档 */ type TotalTokenUsage struct { EmbeddingUsage *ModelTokenUsage `json:"embedding_token_usage,omitempty"` RerankUsage *int64 `json:"rerank_token_usage,omitempty"` LLMUsage *ModelTokenUsage `json:"llm_token_usage,omitempty"` RewriteUsage *ModelTokenUsage `json:"rewrite_token_usage,omitempty"` } /* 检索接口返回切片的详情,详细介绍见官方文档 */ type CollectionSearchResponseItem struct { Id string `json:"id"` Content string `json:"content"` MdContent string `json:"md_content,omitempty"` Score float64 `json:"score"` PointId string `json:"point_id"` OriginText string `json:"origin_text,omitempty"` OriginalQuestion string `json:"original_question,omitempty"` ChunkTitle string `json:"chunk_title,omitempty"` ChunkId int `json:"chunk_id"` ProcessTime int64 `json:"process_time"` RerankScore float64 `json:"rerank_score,omitempty"` DocInfo CollectionSearchResponseItemDocInfo `json:"doc_info,omitempty"` RecallPosition int32 `json:"recall_position"` RerankPosition int32 `json:"rerank_position,omitempty"` ChunkType string `json:"chunk_type,omitempty"` ChunkSource string `json:"chunk_source,omitempty"` UpdateTime int64 `json:"update_time"` ChunkAttachmentList []ChunkAttachment `json:"chunk_attachment,omitempty"` TableChunkFields []PointTableChunkField `json:"table_chunk_fields,omitempty"` OriginalCoordinate *ChunkPositions `json:"original_coordinate,omitempty"` } type CollectionSearchResponseItemDocInfo struct { Docid string `json:"doc_id"` DocName string `json:"doc_name"` CreateTime int64 `json:"create_time"` DocType string `json:"doc_type"` DocMeta string `json:"doc_meta,omitempty"` Source string `json:"source"` Title string `json:"title,omitempty"` } type ChunkAttachment struct { UUID string `json:"uuid,omitempty"` Caption string `json:"caption"` Type string `json:"type"` Link string `json:"link,omitempty"` } type PointTableChunkField struct { FieldName string `json:"field_name"` FieldValue interface{} `json:"field_value"` } type ChunkPositions struct { PageNo []int `json:"page_no"` BBox [][]float64 `json:"bbox"` } /* 模型生成(LLM)请求参数结构体,详细介绍见官方文档 */ type CollectionChatCompletionRequest struct { Model string `json:"model"` ModelVersion string `json:"model_version"` APIKey string `json:"api_key"` Messages []MessageParam `json:"messages"` MaxTokens int32 `json:"max_tokens"` Temperature float32 `json:"temperature"` Stream bool `json:"stream"` ReturnTokenUsage bool `json:"return_token_usage"` } /* 模型生成(LLM)请求参数结构体,详细介绍见官方文档 */ type CollectionChatCompletionResponse struct { Code int64 `json:"code"` Message string `json:"message,omitempty"` Data *CollectionChatCompletionResponseData `json:"data,omitempty"` } type CollectionChatCompletionResponseData struct { GenerateAnswer string `json:"generated_answer"` // 模型生成文本 Usage string `json:"usage"` ReasoningContent string `json:"reasoning_content,omitempty"` //模型推理过程内容,仅推理模型会有 } type ModelTokenUsage struct { PromptTokens int64 `json:"prompt_tokens"` // 请求文本的分词数 CompletionTokens int64 `json:"completion_tokens"` // 生成文本的分词数, 对话模型才有值, 其他模型都是0 TotalTokens int64 `json:"total_tokens"` // PromptTokens + CompletionTokens } func ParseJsonUseNumber(input []byte, target interface{}) error { var d *json.Decoder var err error d = json.NewDecoder(bytes.NewBuffer(input)) if d == nil { return fmt.Errorf("ParseJsonUseNumber init NewDecoder failed") } d.UseNumber() err = d.Decode(&target) if err != nil { return fmt.Errorf("ParseJsonUseNumber Decode failed, err: %s", err.Error()) } return nil } func SerializeToJsonBytesUseNumber(source interface{}) ([]byte, error) { buf := new(bytes.Buffer) encoder := json.NewEncoder(buf) err := encoder.Encode(source) if err != nil { return nil, err } return buf.Bytes(), nil } // scanDoubleCRLF 是一个 bufio.SplitFunc,用于分隔 \r\n\r\n func scanDoubleCRLF(data []byte, atEOF bool) (advance int, token []byte, err error) { // 查找 \r\n\r\n 分隔符 if i := bytes.Index(data, []byte("\r\n\r\n")); i >= 0 { // 返回位置后的分隔符 return i + 4, data[0:i], nil } if atEOF && strings.Contains(string(data), "\"end\":true") { return len(data), data, nil } return 0, nil, nil } // isVisionModel 检查是否是视觉模型 func isVisionModel(modelName string) bool { return strings.Contains(modelName, "vision") } var AK = "your ak" var SK = "your sk" var KnowledgeBaseDomain = "api-knowledgebase.mlp.cn-beijing.volces.com" var SearchKnowledgePath = "/api/knowledge/collection/search_knowledge" // 知识库检索接口,建议您首次接入时使用该检索接口,其他检索接口后续不再进行维护 var ChatCompletionPath = "/api/knowledge/chat/completions" // 大模型对话接口,可以和检索接口接合串联RAG流程,也可以单独使用进行生成 var ModelName = "Doubao-pro-32k" // 模型名称,如果您想使用自己的私有ep,可以赋值为私有EndpointID,格式(ep-xxxx-xxxx) var APIKey = "your api_key" // 如果您使用的是自己的私有ep,需要传入api_key // BasePrompt 是基础提示词,您可以根据你的需求进行修改或者替换,不过需要注意留下 {prompt} 占位符,用于后续拼接检索结果 var BasePrompt = `# 任务 你是一位在线客服,你的首要任务是通过巧妙的话术回复用户的问题,你需要根据「参考资料」来回答接下来的「用户问题」,这些信息在 XML tags 之内,你需要根据参考资料给出准确,简洁的回答。 你的回答要满足以下要求: 1. 回答内容必须在参考资料范围内,尽可能简洁地回答问题,不能做任何参考资料以外的扩展解释。 2. 回答中需要根据客户问题和参考资料保持与客户的友好沟通。 3. 如果参考资料不能帮助你回答用户问题,告知客户无法回答该问题,并引导客户提供更加详细的信息。 4. 为了保密需要,委婉地拒绝回答有关参考资料的文档名称或文档作者等问题。 # 任务执行 现在请你根据提供的参考资料,遵循限制来回答用户的问题,你的回答需要准确和完整。 # 参考资料 {prompt} ` /* 在生成提示词时,如果您希望自定义某些字段拼接至Prompt中,可以在这里进行配置,如果您想获得较好的生成效果,建议: 1.如果您的知识库中的数据为非结构化数据,SystemFields 中可以传入 doc_name, title, chunk_title, content等字段,其中 content 字段为必传字段,其他为可选字段 2.如果您的知识库中的数据为结构化数据,SystemFields 中可以传入title字段(可选),SelfDefineFields字段来源于为结构化数据的表头字段,表头字段中索引字段需要必传,非索引字段可以不传 */ var PromptExtraContextExample = PromptExtraContext{ SystemFields: []string{ SysFieldDocName, // 文档名称 可选 SysFieldTitle, // 文档标题 可选 SysFieldChunkTitle, // 文档切片标题 可选 SysFieldContent, // 文档切片内容 必传 }, SelfDefineFields: []string{ "表头字段-1", // 设置为索引字段需要必传,非索引字段可以选择传入 "表头字段-n", }, } var Query = "your query" // 您的提问 var CollectionName = "your collection" // 知识库名称,前端界面可获取 var Project = "default" // 知识库所属项目,前端界面可获取 var ResourceID = "your resource id" // 知识库ID,前端界面可获取 func PrepareRequest(method string, path string, body []byte) *http.Request { u := url.URL{ Scheme: "https", Host: KnowledgeBaseDomain, Path: path, } req, _ := http.NewRequest(strings.ToUpper(method), u.String(), bytes.NewReader(body)) req.Header.Add("Accept", "application/json") req.Header.Add("Content-Type", "application/json") req.Header.Add("Host", KnowledgeBaseDomain) credential := base.Credentials{ AccessKeyID: AK, SecretAccessKey: SK, Service: "air", Region: "cn-north-1", } req = credential.Sign(req) return req } /* 知识库检索请求参数生成:以下详细展示了部分参数的传递规则,其余参数请参考官方接口文档,如果您想快速接入进行测试,可以只传入参数集合的最小集,其余参数可以使用默认值。 必传参数如下: 1. resourceId (也可使用resource_id 或 name + project, 二选一) 2. query:用户问题 */ func GenerateSearchKnowledgeReqParams() CollectionSearchKnowledgeRequest { return CollectionSearchKnowledgeRequest{ Name: CollectionName, // 知识库名称 Project: Project, // 知识库项目名称 //ResourceId: ResourceID, // 知识库resource_id (二选一,查询时,可使用resource_id 或 name + project) Query: Query, // 用户问题 Limit: 10, // 返回数量, 不传递默认返回10条 DenseWeight: 0.5, //混合搜索的权重 Preprocessing: PreProcessing{ NeedInstruction: true, ReturnTokenUsage: true, Rewrite: false, // 问题改写开关,默认不开启 Messages: []MessageParam{ // 仅在使用改写或意图识别时需要传且必传Messages { Role: "system", Content: ChatCompletionMessageContent{}, }, { Role: "user", Content: ChatCompletionMessageContent{ StringValue: &Query, }, }, }, }, Postprocessing: PostProcessing{ RerankSwitch: false, // 重排开关,默认不开启 RetrieveCount: 25, //进入重排的切片数量,重排打开时生效,需要大于limit,当limit=10,默认值为25 GetAttachmentLink: true, // 是否返回原始图片,仅当创建知识库开启 OCR 时生效,否则自动跳过图片 ChunkGroup: true, //是否对召回切片按照文档进行聚合 ChunkDiffusionCount: 0, //切片扩散数量-是否召回切片的临近切片,如 1 代表额外召回当前切片的上下各一个切片 }, } } func SearchKnowledge(ctx context.Context) (*CollectionSearchKnowledgeResponse, error) { searchKnowledgeReqParams := GenerateSearchKnowledgeReqParams() searchKnowledgeReqParamsBytes, err := SerializeToJsonBytesUseNumber(searchKnowledgeReqParams) if err != nil { return nil, err } req := PrepareRequest("POST", SearchKnowledgePath, searchKnowledgeReqParamsBytes) client := &http.Client{Timeout: 10 * time.Second} resp, err := client.Do(req) if err != nil { return nil, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } var searchKnowledgeResp *CollectionSearchKnowledgeResponse err = ParseJsonUseNumber(body, &searchKnowledgeResp) if err != nil { return nil, err } return searchKnowledgeResp, nil } func GenerateChatCompletionReqParams(stream bool, messages []MessageParam) *CollectionChatCompletionRequest { return &CollectionChatCompletionRequest{ Model: ModelName, // 如果使用私有ep,此处替换为私有ep即可,格式 ep-xxx-xxx ModelVersion: "", // 模型版本,使用公有接入点时,可以选择指定模型版本,不指定则服务会自动指定默认版本 Stream: stream, // 模型结果是否流式返回 ReturnTokenUsage: true, // 是否返回token使用情况 MaxTokens: 4096, // 最大token数 Temperature: 0.7, // 模型温度,取值范围0~1,值越大随机性越大 APIKey: APIKey, // 使用私有ep时,必须传递此参数才能生效 Messages: messages, // 模型对话信息 } } // 非流式调用 func ChatCompletion(ctx context.Context, messages []MessageParam) (*CollectionChatCompletionResponse, error) { chatCompletionReqParams := GenerateChatCompletionReqParams(false, messages) chatCompletionReqParamsBytes, err := SerializeToJsonBytesUseNumber(chatCompletionReqParams) if err != nil { return nil, err } request := PrepareRequest("POST", ChatCompletionPath, chatCompletionReqParamsBytes) client := &http.Client{ Timeout: time.Second * 120, } resp, err := client.Do(request) if err != nil { return nil, err } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { return nil, err } var chatCompletionResp *CollectionChatCompletionResponse err = ParseJsonUseNumber(body, &chatCompletionResp) if err != nil { return nil, err } return chatCompletionResp, nil } // 流式调用 func ChatCompletionStream(ctx context.Context, messages []MessageParam) (answer string, usage *ModelTokenUsage, err error) { chatCompletionReqParams := GenerateChatCompletionReqParams(true, messages) chatCompletionReqParamsBytes, err := SerializeToJsonBytesUseNumber(chatCompletionReqParams) if err != nil { return "", nil, err } request := PrepareRequest("POST", ChatCompletionPath, chatCompletionReqParamsBytes) client := &http.Client{ Timeout: time.Second * 120, } request.Header.Set("Accept", "text/event-stream") resp, err := client.Do(request) if err != nil { return "", nil, err } defer resp.Body.Close() // 读取流式返回 scanner := bufio.NewScanner(resp.Body) // 指定分隔符函数 scanner.Split(scanDoubleCRLF) var answerBuilder strings.Builder var modelTokenUsage ModelTokenUsage buf := make([]byte, 0, 150*1024) scanner.Buffer(buf, 150*1024) // 可以按需调整scanner的大小 // 读取数据 for scanner.Scan() { streamLine := scanner.Text() if len(streamLine) < 5 { continue } streamJson := streamLine[5:] var chatCompletionResponse CollectionChatCompletionResponse err := ParseJsonUseNumber([]byte(streamJson), &chatCompletionResponse) if err != nil { return "", nil, err } // 获取流式返回的内容 fmt.Println(chatCompletionResponse.Data.GenerateAnswer) answerBuilder.WriteString(chatCompletionResponse.Data.GenerateAnswer) // 最后一条流式返回中,携带本次请求token使用信息 if chatCompletionResponse.Data.Usage != "" { err := ParseJsonUseNumber([]byte(chatCompletionResponse.Data.Usage), &modelTokenUsage) if err != nil { return "", nil, err } } } if err := scanner.Err(); err != nil { return "", nil, err } return answerBuilder.String(), &modelTokenUsage, nil } // getContentForPrompt 生成内容提示 func getContentForPrompt(item *CollectionSearchResponseItem, imageNum int) string { content := item.Content if item.OriginalQuestion != "" { return fmt.Sprintf("当询问到相似问题时,请参考对应答案进行回答:问题:“%s”。答案:“%s”", item.OriginalQuestion, content) } if imageNum > 0 && len(item.ChunkAttachmentList) > 0 && item.ChunkAttachmentList[0].Link != "" { placeholder := fmt.Sprintf("图片%d", imageNum) return content + placeholder } return content } func GeneratePrompt(resp *CollectionSearchKnowledgeResponse) (string, []string, error) { if resp == nil { return "", nil, fmt.Errorf("response is nil") } if resp.Code != 0 { return "", nil, fmt.Errorf(resp.Message) } var promptBuilder strings.Builder var imageURLs []string usingVLM := isVisionModel(ModelName) imageCnt := 0 for _, point := range resp.Data.ResultList { // 对vision模型需要额外处理图片链接 if usingVLM && len(point.ChunkAttachmentList) > 0 { link := point.ChunkAttachmentList[0].Link if link != "" { imageURLs = append(imageURLs, link) imageCnt++ } } // 处理系统字段 docInfo := point.DocInfo // 拼接用户指定的系统字段 for _, sysField := range PromptExtraContextExample.SystemFields { switch sysField { case SysFieldDocName: promptBuilder.WriteString(fmt.Sprintf("%s: %s\n", sysField, docInfo.DocName)) case SysFieldTitle: promptBuilder.WriteString(fmt.Sprintf("%s: %s\n", sysField, docInfo.Title)) case SysFieldChunkTitle: promptBuilder.WriteString(fmt.Sprintf("%s: %s\n", sysField, point.ChunkTitle)) case SysFieldContent: promptBuilder.WriteString(fmt.Sprintf("%s: %s\n", sysField, getContentForPrompt(point, imageCnt))) } } // 结构化数据- 拼接用户指定的自定义字段 for _, selfField := range PromptExtraContextExample.SelfDefineFields { for _, tableChunkField := range point.TableChunkFields { if tableChunkField.FieldName == selfField { promptBuilder.WriteString(fmt.Sprintf("%s: %v\n", tableChunkField.FieldName, tableChunkField.FieldValue)) } } } promptBuilder.WriteString("---\n") } // 基础提示词模板替换 finalPrompt := strings.Replace(BasePrompt, "{prompt}", promptBuilder.String(), -1) return finalPrompt, imageURLs, nil } // RAG 检索增强生成流程串联 func RAG(ctx context.Context, stream bool) error { // 知识库检索 searchResp, err := SearchKnowledge(ctx) if err != nil { return err } // 生成提示词 prompt, images, err := GeneratePrompt(searchResp) if err != nil { return err } fmt.Printf("提示词:%s\n", prompt) // 生成Chat的Message结构体,拼接message对话, 问题对应role为user,系统对应role为system, 答案对应role为assistant, 内容对应content var messages []MessageParam if len(images) > 0 { // 对于Vision模型,需要将图片链接拼接到Message中 var multiModalMessage []*ChatCompletionMessageContentPart multiModalMessage = append(multiModalMessage, &ChatCompletionMessageContentPart{ Type: ChatCompletionMessageContentPartTypeText, Text: Query, }) for _, imageURL := range images { multiModalMessage = append(multiModalMessage, &ChatCompletionMessageContentPart{ Type: ChatCompletionMessageContentPartTypeImageURL, ImageURL: &ChatMessageImageURL{URL: imageURL}, }) } messages = []MessageParam{ { Role: "system", Content: prompt, }, { Role: "user", Content: multiModalMessage, }, } } else { // 如果使用的是普通的文本LLM模型,使用该分支拼接生成message messages = []MessageParam{ { Role: "system", Content: prompt, }, { Role: "user", Content: Query, }, } } if stream { // 流式调用 answer, usage, err := ChatCompletionStream(ctx, messages) if err != nil { return err } fmt.Printf("大模型流式调用返回结果:%s\n", answer) fmt.Printf("大模型流式调用返回token使用情况:%+v\n", usage) } else { // 非流式调用 ChatCompletionResponse, err := ChatCompletion(ctx, messages) if err != nil { return err } if ChatCompletionResponse.Code != 0 { return fmt.Errorf(ChatCompletionResponse.Message) } answer := ChatCompletionResponse.Data.GenerateAnswer fmt.Printf("非流式大模型返回结果:%s\n", answer) var modelTokenUsage ModelTokenUsage err = ParseJsonUseNumber([]byte(ChatCompletionResponse.Data.Usage), &modelTokenUsage) if err != nil { return err } fmt.Printf("非流式大模型返回token使用情况:%+v\n", modelTokenUsage) } return nil } func main() { ctx := context.Background() // 仅使用知识库检索 //searchResp, err := SearchKnowledge(ctx) //if err != nil { // fmt.Printf("search knowledge failed: %v", err) // return //} //searchRespStr, _ := SerializeToJsonBytesUseNumber(searchResp) //fmt.Printf("知识库检索结果: %v", string(searchRespStr)) // RAG流程-非流式 //err := RAG(ctx, false) //if err != nil { // fmt.Errorf("RAG err: %v", err) // return //} // RAG流程-流式 err := RAG(ctx, true) if err != nil { fmt.Errorf("RAG err: %v", err) return } }