第二篇讲解了如何训练自己的知识库。本文讲解如何编写第一个属于自己的agent项目。即编写一个网页,通过网页来调用deepseek的接口实现对话。
一.架构图
因版本问题,前端不用easyi,实际使用reactor
二.规划设计
接口:为了实现文字一个一个出现的效果,使用流式接口输出,配合reactor方式。
模型:由于本地安装的是 deepseek-r1:1.5B,所以在调用接口的时候,就用这个模型。可以根据自己的实际情况修改。
模型支持引擎:ollama。需要本地启动ollama install deepseek-r1:1.5B 之后,成功之后再开始代码的调试
前后端架构:前后端一体。为了方便项目的开发和展示,本文不再区分前后端,而是使用前后端一体的项目。
后端:springboot
前端:reactor
业务:实现一个对话框页面,在页面上和部署在本地的大模型对话
支持组件:maven 3.9.1 jdk 11 (提前安装好,本文不再赘述)
开发工具:idea
三.创建项目
开发工具:idea ,提前下载并安装好。本文不在赘述
步骤:1.创建项目 右上角, FILE -- NEW -- PROJECT
选择spring项目,使用初始化的集合工具生成项目,具体如下:
对项目进行命名,包管理工具的 type 选择maven,记着选择jdk和java的版本要一致,本文使用的是jdk11.如果不想处理版本冲突的问题,可以跟着本文来做,不要随便选用其他版本。因为当下jdk已经到jdk21了,高版本的springboot已经不再支持jdk17以下的版本了。所以一定要注意版本选择,不能出现错误,否则可能会出现很多错误,解决起来比较棘手。
2.选择spring组件
计入界面之后,可以先简单勾选几个,不要着急勾选太多,也不用太在意版本,等进入以后,可以使用我提供的pom文件,替换一下pom文件即可
3.等待创建项目
成功后,是这个样子。
3.配置项目,包括maven版本和jdk版本等
为了保证项目使用的是我们本地的maven和jdk(已经提前安装好的),需要在项目中配置一下。
先配置maven:
点击右上角
点setting
按下图的顺序选择
配置jdk
从setting中进入本页面,分别把这几个里面涉及到jdk版本的,都勾选到本地版本上
再进入下面界面:
按图上的顺序配置jdk版本
配置完之后,进入项目中
项目创建完毕
接下来编写代码
三.代码编写
1.测试ollama服务已启动:
使用apifox测试工具,按图中的方式测试一下接口,是否返回正确的结果。
如果没有返回,请翻看第二篇文章,保证ollama或者其他的工具,使模型可调用api接口
2.编写后端代码
核心代码如下,详情请私信,给项目源码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.global-fairy</groupId><artifactId>ds-web</artifactId><version>0.0.1-SNAPSHOT</version><name>ds-web</name><description>ds-web</description><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.0</version> <!-- 确保使用兼容的Spring Boot版本 --></parent><properties><java.version>11</java.version><spring-boot.version>2.7.0</spring-boot.version><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding><project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding></properties><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-webflux</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jdbc</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-validation</artifactId></dependency><dependency><groupId>com.vaadin</groupId><artifactId>vaadin-spring-boot-starter</artifactId></dependency><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.3.0</version></dependency><!-- MySQL Connector --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.23</version></dependency><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.26</version><scope>compile</scope></dependency><dependency><groupId>com.squareup.okhttp3</groupId><artifactId>okhttp</artifactId><version>3.14.9</version></dependency></dependencies><dependencyManagement><dependencies><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-dependencies</artifactId><version>${spring-boot.version}</version><type>pom</type><scope>import</scope></dependency><dependency><groupId>com.vaadin</groupId><artifactId>vaadin-bom</artifactId><version>23.2.10</version> <!-- 使用 Vaadin 的 BOM --><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><build><plugins><plugin><groupId>org.apache.maven.plugins</groupId><artifactId>maven-compiler-plugin</artifactId><version>3.8.1</version><configuration><source>${java.version}</source><target>${java.version}</target><encoding>UTF-8</encoding></configuration></plugin><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId><version>${spring-boot.version}</version><configuration><mainClass>com.globalfairy.dsweb.DsWebApplication</mainClass></configuration></plugin></plugins></build></project>
package com.globalfairy.dsweb.demos.web.action;import com.globalfairy.dsweb.demos.web.bean.ChatRequest;
import com.globalfairy.dsweb.demos.web.bean.ChatResponse;
import com.globalfairy.dsweb.demos.web.service.DeepSeekService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.http.codec.ServerSentEvent;
import org.springframework.web.bind.annotation.*;
import reactor.core.publisher.Flux;import java.io.IOException;
import java.util.UUID;@RestController
@RequestMapping("/api")
public class ChatController {@Autowiredprivate DeepSeekService deepSeekService;@GetMapping(value = "/events", produces = MediaType.TEXT_EVENT_STREAM_VALUE)public Flux<ServerSentEvent<String>> streamEvents(@RequestParam("prompt") String promt) {return deepSeekService.generateStream(promt).map(data -> ServerSentEvent.builder(data).id(UUID.randomUUID().toString()).event("message").build());}
}
package com.globalfairy.dsweb.demos.web.service;import com.globalfairy.dsweb.demos.web.bean.CompletionRequest;
import com.globalfairy.dsweb.demos.web.bean.CompletionResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Flux;import java.time.Duration;@Service
@Slf4j
public class DeepSeekService {private final WebClient webClient;public DeepSeekService(WebClient deepseekWebClient) {this.webClient = deepseekWebClient;}// 普通请求public CompletionResponse generateSync(String prompt) {return webClient.post().bodyValue(CompletionRequest.builder().model("deepseek").prompt(prompt).temperature(0.7).max_tokens(2000).build()).retrieve().bodyToMono(CompletionResponse.class).block();}// 流式响应处理public Flux<String> generateStream(String prompt) {return webClient.post().bodyValue(CompletionRequest.builder().model("deepseek-r1:1.5B").prompt(prompt).stream(true).max_tokens(50).build()).retrieve().bodyToFlux(String.class).timeout(Duration.ofMinutes(5)) // 长时对话场景.onErrorResume(e -> {log.error("API调用异常", e);return Flux.just("服务暂时不可用");});}
}
3.前端代码
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>DeepSeek 聊天</title><style>body {font-family: Arial, sans-serif;margin: 20px;background-color: #f9f9f9;}h1 {text-align: center;color: #333;}.chat-container {display: flex;flex-direction: column;align-items: center;border: 1px solid #ccc;border-radius: 8px;padding: 20px;background-color: #ffffff;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);}#messages {margin-top: 20px;border: 1px solid #ccc;border-radius: 5px;padding: 10px;max-height: 400px;overflow-y: auto;width: 100%;background-color: #f1f1f1;box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.1);display: flex;flex-direction: column;gap: 10px; /* 使用 gap 来增加消息之间的间距 */}.message {padding: 10px;border-radius: 5px;max-width: 80%;white-space: normal; /* 允许正常换行 */word-wrap: break-word; /* 长单词换行 */overflow-wrap: break-word; /* 长单词换行 */background: #c8e6c9; /* 默认背景色 */display: inline-block; /* 使用 inline-block 以保持其流动性 */}.user-message {background-color: #e1f5fe;align-self: flex-end;text-align: right;border: 1px solid #b3e5fc;}.response-message {align-self: flex-start;text-align: left;border: 1px solid #a5d6a7;}input[type="text"] {padding: 10px;width: 70%;border-radius: 5px;border: 1px solid #ccc;margin-right: 10px;}button {padding: 10px 15px;border-radius: 5px;border: none;background-color: #007bff;color: white;cursor: pointer;transition: background-color 0.3s;}button:hover {background-color: #0056b3;}.loading {color: #ff9800;font-weight: bold;text-align: center;}</style>
</head>
<body>
<div class="chat-container"><h1>聊天流</h1><div><input type="text" id="prompt" placeholder="输入您的消息" /><button id="start">开始聊天</button></div><div id="messages"></div>
</div><script>let eventSource;document.getElementById('start').addEventListener('click', function() {const prompt = document.getElementById('prompt').value;if (!prompt) {alert("请输入消息。");return;}if (eventSource) {eventSource.close(); // 关闭之前的连接}// 清空输入框document.getElementById('prompt').value = '';// 添加加载状态const messageDiv = document.getElementById('messages');messageDiv.innerHTML += '<span class="loading">加载中...</span>';eventSource = new EventSource(`/api/events?prompt=${encodeURIComponent(prompt)}`);eventSource.onmessage = function(event) {const data = JSON.parse(event.data); // 假设返回的是 JSON 格式const messageDiv = document.getElementById('messages');const responseMessage = document.createElement('span'); // 使用 span 而非 divresponseMessage.classList.add('message', 'response-message');responseMessage.textContent = data.response;messageDiv.appendChild(responseMessage);messageDiv.scrollTop = messageDiv.scrollHeight; // 滚动到最新消息};eventSource.onerror = function(err) {// 仅在连接意外关闭时显示错误if (eventSource.readyState === EventSource.OPEN) {console.error("EventSource 连接失败:", err);const messageDiv = document.getElementById('messages');const errorMessage = document.createElement('span'); // 使用 span 而非 diverrorMessage.classList.add('message', 'error-message');errorMessage.textContent = '连接时出现错误。';messageDiv.appendChild(errorMessage);}eventSource.close(); // 关闭连接};// 监听连接关闭事件eventSource.onclose = function() {console.log("EventSource 连接已关闭");};});
</script>
</body>
</html>
四.运行
启动项目,运行后,浏览器输入:http://localhost:8080/ds/deepseekAgent.html 可访问到页面
五.测试
输入 你好,看结果:
虽然现实的还不是很好看,但是已经能够看到我们想要的效果了,回答的文字慢慢显示在页面上,这样就完成我们的第一个agent了 。如果觉着还是不好看,可以对界面进行优化,本文暂且不讲,会跟后面融合其他类型的大模型时一并修改。
结语:
本文介绍了从0到1,完成一个agent项目。我们可以在此基础上,添加业务能力,这样就可以实现deepseek+业务了。下一篇,结合编剧的场景,实现一个编剧的智能体。