1
头图

前言

在当今信息爆炸的时代,快速而高效地阅读文档和整理信息变得极其重要。专业人士、学生和学术研究者通常需要阅读大量的资料,而这些文档往往篇幅冗长、内容专业,需要耗费大量时间才能完全理解。特别是面对技术文档、学术论文或行业报告时,即使是领域专家也常常需要反复阅读才能掌握核心内容。

随着 AI 技术的发展,我们是否可以让它帮忙提升阅读效率?当然可以!

我开发了一款 AI 智能阅读助手,它是一个 浏览器插件,能够阅读网页内容和 PDF 文件,然后通过 AI 进行即时问答,甚至还能和它多轮对话,深入理解内容。

本文将详细介绍 AI 智能阅读助手的 项目概述、技术架构、核心功能实现,以及 如何借助腾讯云 DeepSeek APIAI 在阅读场景里发挥最大作用。

准备好了吗?准备一杯你最喜欢的咖啡或茶,随着本文一探究竟吧。

项目概述

功能介绍

AI 智能阅读助手 是一款 浏览器插件,具备以下核心功能:

  • 网页内容解析与问答:提取网页文本,并基于腾讯云 DeepSeek API 进行即时问答。
  • PDF 解析与内容问答:上传 PDF 文件,AI 解析内容,并提供精准问答。
  • 多轮对话与上下文理解:支持与 AI 进行多轮交互,理解用户意图,实现更自然的阅读体验。
  • 历史对话:支持查看历史对话并继续问答。

应用场景

  • 学生与研究人员:快速阅读论文,提取关键信息,进行文献问答。
  • 职场人士:高效浏览行业报告和领域文章,获取精准数据和分析。

效果演示

阅读当前网页文档

阅读PDF文件

历史对话

技术架构

AI 智能阅读助手前端浏览器插件 + 后端 API 服务 组成:

  • 前端(浏览器插件):负责内容传递、用户交互。核心框架:WXT
  • 后端(Go API):提供智能问答、文本解析等 AI 对话能力。核心技术:GoGinMongoDB 以及 腾讯云-知识引擎原子能力

腾讯云 DeepSeek API 在助手中的应用

腾讯云 DeepSeek API 介绍

腾讯云知识引擎原子能力 提供了 DeepSeek API 接口,我们可以通过腾讯云提供的 SDK 进行调用。同时,腾讯云还额外封装了DeepSeek OpenAI 对话接口,兼容了 OpenAI 的接口规范,这意味着我们可以直接使用 OpenAI 官方提供的 SDK 来调用。仅需要将 base_urlapi_key 替换成相关配置,不需要对应用做额外修改。

计费说明:

  • DeepSeek-R1 模型 | 输入:0.004 元 / 千 token | 输出(含思维链):0.016 元 / 千 token
  • DeepSeek-V3 模型 | 输入:0.002 元/ 千 token | 输出:0.008 元 / 千 token

腾讯云 DeepSeek API 的作用

腾讯云 DeepSeek API助手 里的主要作用包括:

  • 内容总结:快速对文档进行总结,提高阅读效率。
  • 即时问答:迅速回答用户的问题。
  • 多轮对话:保持上下文,支持深入理解用户意图。

腾讯云大模型知识引擎的实时文档解析 API 的应用

腾讯云大模型知识引擎的实时文档解析 API 支持将图片或PDF文件转换成Markdown格式文件,可解析包括表格、公式、图片、标题、段落、页眉、页脚等内容元素,并将内容智能转换成阅读顺序。

实时文档解析 API 在助手里的作用:

  • PDF 文档解析:将 PDF 文档内容转成大模型更易于理解的 Markdown 结构化的格式。

核心功能实现

即时问答与多轮交互

前端与后端通信(SSE 实现流式输出)

腾讯云 DeepSeek API 支持流式响应,为了提升对话体验,AI 智能阅读助手在前端采用 Server-Sent Events(SSE) 方式与后端进行通信,以便 AI 的响应能够以流式形式逐步返回,实现打字机的效果,而不是一次性加载整个回答。这样,我们就可以更快地看到 AI 生成的内容,提高交互的流畅性。

核心代码实现如下所示:

发起对话
const completions = async (message: string) => {
  if (!chatId.value) return;


  const requestBody = {
    prompt: message,
    ...(props.sessionData?.type === 'webpage' && message === '' && { web_url: props.sessionData.source }),
    ...(props.sessionData?.type === 'file' && message === '' &&{

      media: {
        doc_type: props.sessionData.fileType,

        file_name: props.sessionData.fileName,
        path: props.sessionData.source
      }
    })
  };


  try {
    const response = await fetch(`${API_BASE}/chats/${chatId.value}/completions`, {
      method: 'POST',

      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(requestBody)
    });

    if (!response.ok) throw new Error(`HTTP错误: ${response.status}`);

    if (!response.body) throw new Error('无法获取响应流');


    const aiMessageId = generateId();
    messages.value.push({
      id: aiMessageId,
      content: '',
      reasoningContent: '',
      isUser: false,
      timestamp: new Date(),

      isComplete: false,
      showReasoning: false
    });

    await processSSEStream(response.body.getReader(), aiMessageId);
  } catch (error) {
    console.error('API请求失败:', error);

    showError(DEFAULT_ERROR_MSG);
    throw error;

  }
};

代码解释:

  • 初始验证检查

    • 检查对话 ID 是否存在,如果不存在则终止函数执行
    • 确保只在有效会话中进行对话 API 的调用
  • 构建请求体

    • 创建包含用户消息的请求对象
    • 根据会话类型动态添加不同参数:

      • 网页分析:当消息为空时添加网页 URL
      • 文件分析:当消息为空时添加文件类型、名称和路径
  • 发起API请求

    • 使用 fetch 向后端 API 发送 POST 请求
    • 设置适当的请求头和序列化的请求体
  • 错误处理

    • 检查 HTTP响应状态
    • 验证响应是否包含可读取的数据流
  • 创建新的消息体

    • 生成唯一的消息 ID
    • 向消息列表添加初始为空的 AI 回复
    • 设置初始状态标记(未完成、不显示推理过程等)
  • 处理 SSE数据流

    • 调用 processSSEStream函数处理响应流
    • 传入流读取器和 AI 消息 ID
    • 实时更新 UI上的AI回复内容
  • 异常捕获

    • 捕获并记录任何 API请求过程中的错误
    • 向用户显示友好的错误提示
    • 将错误向上传播以便进一步处理
处理流式响应
const processSSEStream = async (reader: ReadableStreamDefaultReader, aiMessageId: string) => {
  currentReader.value = reader;
  isGenerating.value = true;
  const decoder = new TextDecoder();
  let buffer = '';

  try {
    while (true) {
      const { done, value } = await reader.read();
      if (done) break;


      buffer += decoder.decode(value, { stream: true });

      while (true) {
        const eventEndIndex = buffer.indexOf('\n\n');
        if (eventEndIndex === -1) break;

        const eventData = buffer.substring(0, eventEndIndex);
        buffer = buffer.substring(eventEndIndex + 2);

        const lines = eventData.split('\n').filter(line => line.trim());
        let eventType = 'message';
        let jsonData = '';


        for (const line of lines) {
          if (line.startsWith('event:')) {

            eventType = line.substring(6).trim();
          } else if (line.startsWith('data:')) {
            jsonData += line.substring(5).trim();

          }
        }

        if (eventType === 'close') {
          try {
            const closeData = JSON.parse(jsonData);
            if (closeData.error) showError(`服务端关闭连接: ${closeData.error}`);
          } catch (e) {
            console.error('关闭事件解析失败:', e);
          }
          return;

        }

        if (jsonData) {
          try {
            const data = JSON.parse(jsonData) as ChatMessage;
            const targetIndex = messages.value.findIndex(m => m.id === aiMessageId);
            if (targetIndex === -1) return;

            const original = messages.value[targetIndex];
            
            // 处理reasoningContent和content的打字机效果

            messages.value[targetIndex] = {
              ...original,
              content: data.content ? original.content + data.content : original.content,
              reasoningContent: data.reasoning_content ? 
                (original.reasoningContent || '') + data.reasoning_content : 

                original.reasoningContent,
              timestamp: new Date((data.created_at || Date.now()) * 1000),
              showReasoning: original.showReasoning || !!data.reasoning_content
            };
            scrollToBottom();
          } catch (e) {

            console.error('SSE数据解析失败:', e, '原始数据:', jsonData);
          }
        }
      }
    }
  } catch (error) {
    console.error('读取流数据失败:', error);
    showError('连接异常中断,请重试');
  } finally {

    reader.releaseLock();
    currentReader.value = null;

    isLoading.value = false;

    isGenerating.value = false;
    const targetIndex = messages.value.findIndex(m => m.id === aiMessageId);
    if (targetIndex > -1) messages.value[targetIndex].isComplete = true;
  }
};

代码解释:

  • 初始化

    • 记录当前流读取器,并标记 AI 正在生成内容。
    • 创建解码器和缓冲区,用于处理流式数据。
  • 读取和解析 SSE 数据流

    • 通过 reader.read() 持续读取数据并解码。
    • 使用缓冲区存储未完全解析的数据,并按 \n\n 解析完整的 SSE 事件。
    • 识别事件类型 (message 或 close),提取 data 部分。
  • 处理关闭事件

    • 如果服务器发送 close 事件,检查是否包含错误信息,并向用户提示错误。
    • 终止数据流处理。
  • 更新消息内容

    • 查找 aiMessageId 对应的消息对象,并将新内容追加,实现打字机效果。
    • 处理 reasoningContent(推理内容),更新时间戳,并决定是否显示推理信息。
    • 滚动到底部,确保用户看到最新消息。
  • 异常处理

    • 处理 JSON 解析失败或数据读取异常,并向用户显示错误提示。
  • 清理资源

    • 释放流读取器,重置状态,标记 AI 生成完成。

后端 API 处理(Go + Gin + SSE)

后端采用 Go Gin 框架 处理前端请求,并通过 SSE 返回流式数据。核心代码实现如下所示:

streamData, err := h.serv.ChatCompletion(ctx, userId, chatPrompt)

if err != nil {
    slog.Error(err.Error())
    if errors.Is(err, mongo.ErrNoDocuments) {
        ctx.SSEvent("close", apiwrap.NewErrorResponseBody(404, "chat not exist"))
        return
    } else if errors.Is(err, errors.New("user is chatting")) {
        ctx.SSEvent("close", apiwrap.NewErrorResponseBody(400, "user is chatting"))
        return
    }
    ctx.SSEvent("close", apiwrap.NewResponseBody[any](500, err.Error(), nil))
    return
}
resp, _ := streamData.(*lkeap.ChatCompletionsResponse)
message := domain.ChatMessage{
    CreatedAt: time.Now(),

}

defer func() {
    _ = h.serv.SaveChatMessage(ctx, userId, chatId, message)
}()


for {
    select {
    case <-ctx.Request.Context().Done():
        // 客户端主动断开连接

        slog.Info("客户端断开连接")
        return
    case event, ok := <-resp.Events:

        if !ok {
            // 事件通道关闭,退出循环
            ctx.SSEvent("close", apiwrap.SuccessResponse())
            return
        }
        data := tclkep.ChatCompletionsResponse{}
        err = json.Unmarshal(event.Data, &data)
        if err != nil {
            ctx.SSEvent("close", apiwrap.NewResponseBody[any](500, err.Error(), nil))
            return
        }
        choice := data.Choices[0]
        message.Role = "assistant"
        message.Content += choice.Delta.Content
        message.ReasoningContent += choice.Delta.ReasoningContent
        cm := ChatMessage{
            Role:             choice.Delta.Role,
            Content:          choice.Delta.Content,
            ReasoningContent: choice.Delta.ReasoningContent,
            CreatedAt:        data.Created,
        }
        ctx.SSEvent("message", cm)
        ctx.Writer.Flush()
        slog.Info("chat completion response: ", cm)
    }
}

代码解释:

  • 处理请求并调用聊天服务

    • 通过 h.serv.ChatCompletion(ctx, userId, chatPrompt) 调用聊天服务,获取流式响应数据。
    • 如果请求失败,根据错误类型返回不同的 SSE 事件 close(例如:聊天不存在、用户正在聊天等)。
  • 初始化响应消息

    • 解析 streamData,并创建 message 结构体用于存储 AI 回复。
    • 使用 defer 关键字确保在函数返回前,持久化 message 数据到数据库。
  • 监听 SSE 事件并发送响应

    • 进入 for 循环,不断从 resp.Events 读取 AI 生成的内容。
    • 处理客户端断开:

      • 监听 ctx.Request.Context().Done(),如果客户端主动断开,则退出循环。
    • 解析 AI 返回的数据:

      • 从 resp.Events 读取 event,如果通道关闭,发送 close 事件并终止循环。
      • 使用 json.Unmarshal(event.Data, &data) 解析 JSON 数据,提取 Choices 内容。
    • 更新并发送 AI 回复:

      • 解析 choice.Delta,更新 message.Content 和 message.ReasoningContent
      • 组装 ChatMessage 结构体,发送 SSE 事件 message,并调用 ctx.Writer.Flush() 确保数据立即推送到前端。
  • 终止逻辑

    • 发生错误时,发送 close 事件,返回错误信息。
    • 当 resp.Events 关闭时,发送 close 事件并退出循环。
    • 日志记录每次 AI 生成的响应,便于调试和监控。

多轮交互实现(MongoDB 记录历史对话)

为了让 AI 能够理解对话上下文,每次用户发送消息时,后端系统需要 查询 MongoDB 里的历史对话,并将其与新问题封装后发送给 DeepSeek API

存储聊天记录
  • 采用 MongoDB 存储每个用户的对话记录。
  • 每条消息存入 chats 集合,并记录 用户 ID、对话标题,对话消息,对话时间 等信息

MongoDB 数据结构示例:

{
    "_id": {"$oid": "67d799fc32fa24462017e415"},
    "created_at": {"$date": "2025-03-17T03:41:48.941Z"},
    "title": "Go 语言 mongox 库:简化操作、安全、高效、可扩展、BSON 构建.pdf",
    "updated_at": {"$date": "2025-03-17T03:42:46.570Z"},

    "user_id": "chenmingyong",
    "messages": [

      {
        "_id": {"$oid": "67d799fc32fa24462017e315"},

        "role": "system",
        "content": "\n你是一位专业的文档助手,负责为用户提供关于文档的详细、准确和清晰的回答。你的任务是帮助用户理解、分析和解决与文档相关的任何问题。\n任务目标: \n1. 回答问题 :针对用户提出的问题,提供详细、准确和易于理解的回答。 \n2. 保持专业性 :在回答问题时,始终保持专业、礼貌和客观的态度。 \n回答要求:\n1. 简洁明了 :回答应简洁明了,避免冗长和不必要的细节。 \n2. 结构化 :使用段落、列表或标题来组织信息,确保易于阅读和理解。 \n3. 引用文档 :在回答中引用文档中的具体部分,以支持你的观点或解释。 \n4. 提供上下文 :如果问题涉及文档的特定部分,提供足够的上下文信息,帮助用户理解。\n注意事项: \n1. 准确性 :确保所有回答都基于文档中的实际内容,避免猜测或假设。 \n2. 用户友好 :使用用户易于理解且与文档关联语言,避免使用超纲或复杂的术语。\n",
        "created_at": {"$date": "2025-03-17T03:41:57.648Z"},
        "is_hidden": true
      },
      {
        "content": "\n# 问题描述\n  \n帮我总结一下文档的内容,字数不超过 300 字。\n# 文档\n\n",
        "created_at": {"$date": "2025-03-17T03:41:59.728Z"},
        "is_hidden": false,
        "_id": {"$oid": "67d799fc32fa24462017e215"}
      }
    ]
  }
  
组装对话历史并发送给 DeepSeek API

当用户发送新的问题时,后端会从 MongoDB 获取所有历史对话信息,然后将历史对话合并,之后发送给 DeepSeek API,以保持上下文对话能力。

核心代码实现如下所示:

// 查询历史对话
chat, err := s.repo.FindChatByUserIdAndChatId(ctx, userId, chatPrompt.ChatId)

if err != nil {
    return nil, err
}

// 封装新的对话信息

if len(chat.Messages) == 0 {
    chat.Messages = append(chat.Messages, domain.ChatMessage{
        Role:      "system",
        Content:   promptInfo.SystemPrompt,
        CreatedAt: now,
        IsHidden:  true,
    }, domain.ChatMessage{
        Role:      "user",
        Content:   promptInfo.UserPrompt,
        CreatedAt: now,
        IsHidden:  true,
    })
} else {
    chat.Messages = append(chat.Messages, domain.ChatMessage{
        Role:      "user",
        Content:   promptInfo.UserPrompt,
        CreatedAt: now,
    })
}

messages := slice.Map(chat.Messages, func(idx int, message domain.ChatMessage) *lkeap.Message {
    return &lkeap.Message{
        Content: gkit.ToPtr(message.Content),
        Role:    gkit.ToPtr(message.Role),
    }
})

// 与大模型进行对话
resp, err := s.client.NewChatCompletions("deepseek-r1").
    WithStream(true).
    WithMessages(messages).
    Do()
    

腾讯云 SDK 封装

虽然腾讯云提供的 SDK 能够帮助我们快速接入 API,但为了更符合我自己的编码习惯,我对其进行进一步封装。代码示例如下所示:

  • client 对象封装

    package tclkep
    
    import (
    "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
    "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
    lkeap "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/lkeap/v20240522"
    )
    
    // Client 封装腾讯云 LKEAP 客户端
    type Client struct {
    client *lkeap.Client
    }
    
    // NewLKEAPClient 创建一个新的 LKEAP 客户端
    func NewLKEAPClient(secretId, secretKey, region string) (*Client, error) {
    // 实例化一个认证对象
    credential := common.NewCredential(secretId, secretKey)
    // 实例化一个client选项
    cpf := profile.NewClientProfile()
    cpf.HttpProfile.Endpoint = "lkeap.tencentcloudapi.com"
    // 实例化要请求产品的client对象
    
    client, err := lkeap.NewClient(credential, region, cpf)
    if err != nil {
        return nil, err
    }
    return &Client{client: client}, nil
    }
    
    // NewChatCompletions 创建一个新的聊天完成请求对象
    func (c *Client) NewChatCompletions(model string) *ChatCompletions {
    request := lkeap.NewChatCompletionsRequest()
    request.Model = common.StringPtr(model)
    return &ChatCompletions{
        client:  c,
        request: request,
    }
    }
    
    func (c *Client) NewDocumentParser() *DocumentParser {
    request := lkeap.NewReconstructDocumentSSERequest()
    return &DocumentParser{
        client:  c,
        request: request,
    }
    }
    
  • 腾讯云 DeepSeek API 调用封装

    package tclkep
    
    import (
    "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
    lkeap "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/lkeap/v20240522"
    )
    
    // ChatCompletions 聊天完成接口的构建器,支持流式调用
    type ChatCompletions struct {
    client  *Client
    request *lkeap.ChatCompletionsRequest
    }
    
    // WithMessages 设置消息
    func (b *ChatCompletions) WithMessages(messages []*lkeap.Message) *ChatCompletions {
    b.request.Messages = messages
    return b
    }
    
    // AddMessage 添加单条消息
    func (b *ChatCompletions) AddMessage(role, content string) *ChatCompletions {
    message := &lkeap.Message{Role: common.StringPtr(role), Content: common.StringPtr(content)}
    if b.request.Messages == nil {
        b.request.Messages = []*lkeap.Message{message}
    } else {
        b.request.Messages = append(b.request.Messages, message)
    }
    return b
    
    }
    
    
    // WithStream 设置是否使用流式响应
    func (b *ChatCompletions) WithStream(stream bool) *ChatCompletions {
    b.request.Stream = common.BoolPtr(stream)
    return b
    }
    
    // Do 执行请求
    func (b *ChatCompletions) Do() (*lkeap.ChatCompletionsResponse, error) {
    return b.client.client.ChatCompletions(b.request)
    
    }
    
    type ChatCompletionsResponse struct {
    Choices []Choice `json:"Choices"`
    Created int64    `json:"Created"`
    Id      string   `json:"Id"`
    Model   string   `json:"Model"`
    Object  string   `json:"Object"`
    Usage   Usage    `json:"Usage"`
    }
    
    type Choice struct {
    Delta Delta `json:"Delta"`
    Index int   `json:"Index"`
    }
    
    type Delta struct {
    Content          string `json:"Content,omitempty"`
    ReasoningContent string `json:"ReasoningContent,omitempty"`
    Role             string `json:"Role,omitempty"`
    }
    
    type Usage struct {
    CompletionTokens int `json:"CompletionTokens"`
    
    PromptTokens     int `json:"PromptTokens"`
    TotalTokens      int `json:"TotalTokens"`
    }
    
  • 腾讯云大模型知识引擎的实时文档解析 API 封装

    package tclkep
    
    import (
    "context"
    
    "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
    lkeap "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/lkeap/v20240522"
    )
    
    type DocumentParser struct {
    client  *Client
    request *lkeap.ReconstructDocumentSSERequest
    }
    
    func (b *DocumentParser) WithFileType(fileType string) *DocumentParser {
    b.request.FileType = common.StringPtr(fileType)
    return b
    }
    
    func (b *DocumentParser) WithFileUrl(fileUrl string) *DocumentParser {
    b.request.FileUrl = common.StringPtr(fileUrl)
    return b
    }
    
    func (b *DocumentParser) WithFileBase64(fileBase64 string) *DocumentParser {
    b.request.FileBase64 = common.StringPtr(fileBase64)
    return b
    }
    
    // Do 执行请求
    func (b *DocumentParser) Do(ctx context.Context) (*lkeap.ReconstructDocumentSSEResponse, error) {
    return b.client.client.ReconstructDocumentSSEWithContext(ctx, b.request)
    }
    

以上封装的目的是为了方便实现链式调用,使代码更加简洁、易读,并提高开发效率,例如:

  • 与 DeepSeek 进行对话:

    resp, err := s.client.NewChatCompletions("deepseek-r1").
        WithStream(true).
        WithMessages(messages).
        Do()
    
  • 实时文档解析:

    documentSSEResponse, err := s.client.NewDocumentParser().
        WithFileType(strings.ToUpper(media.DocType)).
        WithFileBase64(base64.StdEncoding.EncodeToString(b)).Do(ctx)
    

网页内容解析与转换

通过结合 Go 语言的 go-readability和 html-to-markdown 两个库,我们可以高效地实现网页内容的解析与格式转换。核心代码实现如下所示:

var article readability.Article
// 读取网页内容
article, err = readability.FromURL(url, 30*time.Second)
if err != nil {
    return promptInfo, fmt.Errorf("failed to readability.FromURL: %w", err)
}
// 将 html 转成 markdown
content, err = htmltomarkdown.ConvertString(article.Content)
if err != nil {
    return promptInfo, fmt.Errorf("failed to htmltomarkdown.ConvertString: %w", err)
}

将网页内容转换为 markdown 格式是因为 AI 大语言模型在处理 结构化文字(如 Markdown)时表现更好

PDF 文件解析与转换

对于 PDF 文件,首先需要上传到服务器得到文件存储的路径,然后在对话时传递给对话接口,接下来通过路径读取文件内容,然后通过 腾讯云知识引擎原子能力 提供的 实时文档解析 API 将 PDF 的内容转换成 markdown 格式,以便大模型更易于理解文档内容。

核心代码实现如下所示:

func (s *ChatService) toMarkdown(ctx context.Context, media domain.Media) (string, string, error) {

    var content string

    filePath := strings.Replace(media.Path, "/static/", "./static/files/", 1)
    // 读取 PDF 文件内容
    b, fileErr := os.ReadFile(filePath)
    if fileErr != nil {
        return "", "", fmt.Errorf("failed to os.ReadFile: %w", fileErr)
    }

    // 调用腾讯云文档解析服务
    documentSSEResponse, err := s.client.NewDocumentParser().
        WithFileType(strings.ToUpper(media.DocType)).
        WithFileBase64(base64.StdEncoding.EncodeToString(b)).Do(ctx)

    if err != nil {

        return "", "", fmt.Errorf("failed to s.client.NewDocumentParser.Do: %w", err)

    }
    // 流式响应
    var documentRecognizeResultUrl string

    for event := range documentSSEResponse.Events {
        type EventResponse struct {
            ProgressMessage            string `json:"ProgressMessage"`
            DocumentRecognizeResultUrl string `json:"DocumentRecognizeResultUrl"`
            StatusCode                 string `json:"StatusCode"`
        }
        eResp := EventResponse{}
        err = json.Unmarshal(event.Data, &eResp)
        if err != nil {
            panic(err)
        }

        if eResp.ProgressMessage == "完成文档解析" && eResp.StatusCode == "Success" {
            documentRecognizeResultUrl = eResp.DocumentRecognizeResultUrl
            break
        }
    }
    // 下载文件
    get, hErr := http.Get(documentRecognizeResultUrl)
    if hErr != nil {
        return "", "", fmt.Errorf("failed to http.Get: %w", hErr)
    }
    defer get.Body.Close()
    // 2. 读取 ZIP 文件内容
    body, err := io.ReadAll(get.Body)

    if err != nil {
        return "", "", err
    }

    // 3. 解压 ZIP 文件
    reader := bytes.NewReader(body)
    zipReader, err := zip.NewReader(reader, int64(len(body)))
    if err != nil {
        return "", "", err
    }

    // 4. 遍历 ZIP 文件中的文件
    for _, file := range zipReader.File {
        // 检查文件扩展名是否为 .md
        if strings.HasSuffix(file.Name, ".md") {
            // 5. 打开文件
            fileReader, err := file.Open()
            if err != nil {
                return "", "", err
            }
            defer fileReader.Close()

            // 6. 读取文件内容

            md, err := io.ReadAll(fileReader)

            if err != nil {
                return "", "", err
            }

            content = string(md)
            break
        }
    }
    return media.FileName, content, nil
}

代码解释:

  • 读取 PDF 文件

    • 解析 media.Path 生成本地文件路径 filePath
    • 使用 os.ReadFile(filePath) 读取 PDF 文件内容,并进行错误处理。
  • 调用腾讯云文档解析 API

    • 通过 s.client.NewDocumentParser() 调用 API,发送 PDF 数据(Base64 编码)。
    • 监听流式响应:

      • 遍历 documentSSEResponse.Events,等待解析完成的事件。
      • 提取 DocumentRecognizeResultUrl(解析结果的下载链接)。
  • 下载解析结果(ZIP 文件)

    • 通过 http.Get(documentRecognizeResultUrl) 下载 ZIP 文件,并读取内容。
    • 使用 io.ReadAll(get.Body) 解析 ZIP 数据。
  • 解压 ZIP 并提取 Markdown 文件

    • 通过 zip.NewReader() 解析 ZIP 文件内容。
    • 遍历 zipReader.File 查找 .md 文件。
    • 读取 Markdown 文件内容,并存入 content 变量。
  • 返回结果

    • 返回文件名 media.FileName 和解析出的 Markdown 内容 content
    • 处理可能的错误(文件读取、API 调用、ZIP 解压等)。

system prompt 与 user prompt 的封装

在 AI 智能阅读助 里,需要封装两种类型的 promptsystem prompt 和 user prompt

  • system prompt

    • 由开发者或系统设定的隐藏指令,用于定义 AI 的角色、行为准则或回答风格。
    • 在和腾讯云 DeepSeek 第一次对话时,需要携带 system prompt参数,以便让 AI 提供更为精确和定制化的回答。
    • system prompt 参考内容如下:

你是一位专业的文档助手,负责为用户提供关于文档的详细、准确和清晰的回答。你的任务是帮助用户理解、分析和解决与文档相关的任何问题。

任务要求: 

1. 回答问题 :针对用户提出的问题,提供详细、准确和易于理解的回答。 

2. 保持专业性 :在回答问题时,始终保持专业、礼貌和客观的态度。 

回答要求:

1. 简洁明了 :回答应简洁明了,避免冗长和不必要的细节。 

2. 结构化 :使用段落、列表或标题来组织信息,确保易于阅读和理解。 

3. 引用文档 :在回答中引用文档中的具体部分,以支持你的观点或解释。 

4. 提供上下文 :如果问题涉及文档的特定部分,提供足够的上下文信息,帮助用户理解。

注意事项: 

1. 准确性 :确保所有回答都基于文档中的实际内容,避免猜测或假设。 

2. 用户友好 :使用用户易于理解且与文档关联语言,避免使用超纲或复杂的术语。

  • user prompt:用户直接输入的请求或问题,明确告知 AI 需要执行的任务。

    • 用户选择 阅读当前网页 或 阅读PDF文件 时,第一次对话的 user prompt 由后端系统封装,格式如下:

      # 问题描述
      
      帮我总结文档的内容。
      
      # 文档
      
      ${document content}

在之后的对话里,user prompt 会被直接指定为 用户输入 的内容。

小结

在这个 AI 智能阅读助手项目中,我使用了腾讯云提供的 DeepSeek API,充分利用了 DeepSeek 大语言模型的自然语言处理能力。这个模型在理解文本内容、提取关键信息方面表现得非常出色,成为了整个项目的核心技术支撑。腾讯云的 API 服务在稳定性和响应速度上也满足了项目需求,接口调用体验非常流畅,价格方面也是非常实惠。

除了腾讯云的DeepSeek API,我还借助了腾讯云大模型知识引擎的 实时文档解析 API 将 PDF 的内容转换成 markdown 格式,以便大模型更易于理解文档内容。腾讯云大模型知识引擎除了 DeepSeek 和文档解析 API 以外,还提供了很多与 AI 应用相关的 API,例如 获取特征向量多轮改写 等。

尽管这个阅读助手目前仍处于原型阶段,功能还有待完善,但通过这次开发实践,我对如何将 AI 能力与实际阅读需求相结合有了更深入的思考。这篇文章的主要目的是记录设计思路和技术选择,希望能为有类似需求的开发者提供一些参考和启发。

持续更新 Go 与 AI 相关的文章,关注我,学习更多的前沿知识。

你好,我是陈明勇,一名热爱技术、乐于分享的开发者,同时也是开源爱好者。

我专注于分享 Go 语言相关的技术知识,同时也会深入探讨 AI 领域的前沿技术。

成功的路上并不拥挤,有没有兴趣结个伴?


陈明勇
29 声望6 粉丝

你好,我是陈明勇,一名热爱技术、乐于分享的开发者,同时也是开源爱好者。