目录

1 获取密钥

2 Unity设置

2.1 创建一个空物体并挂载脚本

2.2 配置对话交互功能

3 测试

4 补档(新增AI具备上下文能力)

4.1 核心修改思路

4.2 具体代码修改

4.2.1 添加历史记录变量

4.2.2 修改核心请求逻辑

4.2.3 添加清空历史的方法


不需要看懂代码如何编写,按照下述步骤复制粘贴即用,简单易用!

1 获取密钥

首先需要申请DeepSeek的API密钥:

  1. 访问DeepSeek平台(https://platform.deepseek.com/api_keys)申请API Key

  2. 成功后会显示Key,请立即妥善保存,因为关闭页面后将无法再次查看完整Key

2 Unity设置

在Unity中创建基本的UI界面

2.1 创建一个空物体并挂载脚本

DeepSeekLink代码:

public class DeepSeekLink : MonoBehaviour
{
    // API配置区域 - 在Inspector面板中设置这些参数
    [Header("API Settings")]
    [SerializeField] private string apiKey = "输入自己创建的密钥"; // DeepSeek API密钥 - 从DeepSeek平台获取
    [SerializeField] private string modelName = "deepseek-chat"; // 使用的模型名称 - deepseek-chat是对话专用模型
    [SerializeField] private string apiUrl = "https://api.deepseek.com/v1/chat/completions"; // API请求地址 - DeepSeek官方API端点

    // 对话参数设置 - 控制AI回复的行为特性
    [Header("Talk Settings")]
    [Range(0, 2)] public float temperature = 0.7f; // 控制生成文本的随机性:0=最确定,1=平衡,2=最随机
    [Range(1, 1000)] public int maxTokens = 150; // 单次回复的最大令牌数:控制回复长度,1个token≈0.75个英文单词

    // 角色设定类 - 定义AI助手的角色和个性
    [System.Serializable]
    public class NPCCharacter
    {
        public string name; // 角色名称 - 在对话中标识AI身份
        [TextArea(3, 10)]
        public string personalityPrompt = "你名字是:小历,是一个熟读中国历史,知识渊博的智能体,个性严谨稳重。"; // 角色个性提示词 - 决定AI的行为方式和知识范围
    }

    [SerializeField] public NPCCharacter npcCharacter; // 在Inspector中配置的角色实例

    // 回调委托定义 - 用于异步处理API响应结果
    public delegate void DialogueCallback(string response, bool isSuccess);

    /// <summary>
    /// 发送对话请求的主方法 - 外部调用的入口点
    /// 使用方法:在其他脚本中调用此方法并传入用户消息和回调函数
    /// 示例:deepSeekManager.SendDialogueRequest("你好", OnAIResponse);
    /// </summary>
    /// <param name="userMessage">玩家输入的内容</param>
    /// <param name="callback">处理响应的回调函数,接收回复内容和成功状态</param>
    public void SendDialogueRequest(string userMessage, DialogueCallback callback)
    {
        // 启动协程处理异步API请求
        StartCoroutine(ProcessDialogueRequest(userMessage, callback));
    }

    /// <summary>
    /// 处理对话请求的协程 - 核心业务逻辑
    /// </summary>
    /// <param name="userInput">用户输入的文本</param>
    /// <param name="callback">回调函数用于返回处理结果</param>
    private IEnumerator ProcessDialogueRequest(string userInput, DialogueCallback callback)
    {
        // 构建消息列表:包含系统角色设定和用户当前输入
        List<Message> messages = new List<Message> {
            new Message { role = "system", content = npcCharacter.personalityPrompt }, // 系统消息:定义AI角色和行为准则
            new Message { role = "user", content = userInput } // 用户消息:当前对话内容
        };

        // 构建API请求体 - 符合DeepSeek API格式要求
        ChatRequest requestBody = new ChatRequest
        {
            model = modelName, // 指定使用的AI模型
            messages = messages, // 传入对话消息列表
            temperature = temperature, // 设置创造性参数
            max_tokens = maxTokens // 设置回复长度限制
        };

        // 将请求体序列化为JSON字符串
        string jsonBody = JsonUtility.ToJson(requestBody);
        // 调试输出:在Console中查看发送的JSON数据(发布时建议注释掉)
        Debug.Log("Sending JSON: " + jsonBody);

        // 创建并配置网络请求
        UnityWebRequest request = CreateWebRequest(jsonBody);
        // 发送请求并等待响应 - yield return使协程在此暂停直到请求完成
        yield return request.SendWebRequest();

        // 检查请求是否出错
        if (IsRequestError(request))
        {
            // 输出错误信息到控制台
            Debug.LogError($"API Error: {request.responseCode}\n{request.downloadHandler.text}");
            // 调用回调函数,传递失败状态
            callback?.Invoke(null, false);
            // 提前退出协程
            yield break;
        }

        // 解析API返回的JSON响应
        DeepSeekResponse response = ParseResponse(request.downloadHandler.text);
        // 检查响应是否有效且包含选择结果
        if (response != null && response.choices.Length > 0)
        {
            // 提取AI回复内容
            string npcReply = response.choices[0].message.content;
            // 调用成功回调,传递回复内容
            callback?.Invoke(npcReply, true);
        }
        else
        {
            // 响应解析失败或为空时的处理
            callback?.Invoke("Unity-YuZhang(陷入沉默)", false);
        }
    }

    /// <summary>
    /// 创建并配置UnityWebRequest对象
    /// </summary>
    /// <param name="jsonBody">要发送的JSON数据</param>
    /// <returns>配置好的UnityWebRequest对象</returns>
    private UnityWebRequest CreateWebRequest(string jsonBody)
    {
        // 将JSON字符串转换为字节数组
        byte[] bodyRaw = System.Text.Encoding.UTF8.GetBytes(jsonBody);
        // 创建POST请求对象
        var request = new UnityWebRequest(apiUrl, "POST");
        // 设置上传处理器:负责发送数据
        request.uploadHandler = new UploadHandlerRaw(bodyRaw);
        // 设置下载处理器:负责接收响应数据
        request.downloadHandler = new DownloadHandlerBuffer();
        // 设置请求头:声明内容类型为JSON
        request.SetRequestHeader("Content-Type", "application/json");
        // 设置认证头:Bearer Token认证方式
        request.SetRequestHeader("Authorization", $"Bearer {apiKey}");
        // 设置接受头:期望接收JSON格式响应
        request.SetRequestHeader("Accept", "application/json");
        return request;
    }

    /// <summary>
    /// 检查网络请求是否出错
    /// </summary>
    /// <param name="request">要检查的UnityWebRequest对象</param>
    /// <returns>true表示请求出错,false表示成功</returns>
    private bool IsRequestError(UnityWebRequest request)
    {
        // 检查所有可能的错误类型
        return request.result == UnityWebRequest.Result.ConnectionError    // 连接错误:网络问题
            || request.result == UnityWebRequest.Result.ProtocolError      // 协议错误:HTTP错误码
            || request.result == UnityWebRequest.Result.DataProcessingError; // 数据处理错误:本地处理失败
    }

    /// <summary>
    /// 解析API返回的JSON响应
    /// </summary>
    /// <param name="jsonResponse">API返回的JSON字符串</param>
    /// <returns>解析后的DeepSeekResponse对象,解析失败返回null</returns>
    private DeepSeekResponse ParseResponse(string jsonResponse)
    {
        try
        {
            // 使用Unity的JsonUtility反序列化JSON字符串
            return JsonUtility.FromJson<DeepSeekResponse>(jsonResponse);
        }
        catch (System.Exception e)
        {
            // 捕获并记录JSON解析异常
            Debug.LogError($"JSON解析失败: {e.Message}\n响应内容:{jsonResponse}");
            return null;
        }
    }

    // ========== 可序列化数据结构定义 ==========

    /// <summary>
    /// API请求数据结构 - 与DeepSeek API要求的格式匹配
    /// </summary>
    [System.Serializable]
    private class ChatRequest
    {
        public string model;           // 模型名称
        public List<Message> messages; // 消息列表
        public float temperature;      // 温度参数
        public int max_tokens;         // 最大令牌数
    }

    /// <summary>
    /// 消息数据结构 - 表示对话中的一条消息
    /// </summary>
    [System.Serializable]
    public class Message
    {
        public string role;    // 消息角色:system/user/assistant
        public string content; // 消息内容
    }

    /// <summary>
    /// API响应数据结构 - 解析DeepSeek返回的JSON
    /// </summary>
    [System.Serializable]
    private class DeepSeekResponse
    {
        public Choice[] choices; // 选择数组,包含AI的回复选项
    }

    /// <summary>
    /// 选择数据结构 - 包含AI生成的一条回复
    /// </summary>
    [System.Serializable]
    private class Choice
    {
        public Message message; // 消息对象,包含AI的回复内容
    }
}
  • temperature:建议0.7-1.0之间,创造性对话用高点,技术问答用低点
  • maxTokens:根据需求调整,短回复50-100,长回复200-500
  • personalityPrompt:详细描述角色特性可获得更符合预期的回复

创建空物体命名:DSLinkObj,添加写好的DeepSeekLink。

2.2 配置对话交互功能

(1)创建脚本NPCInteraction,用于处理交互功能。

代码如下:

public class NPCInteraction : MonoBehaviour
{
    [Header("组件引用")]
    [SerializeField] private DeepSeekLink deepSeekLink;  // 对话管理器 - 引用DeepSeek对话管理脚本
    [SerializeField] private InputField inputField;  // 玩家输入框 - 用于玩家输入对话内容
    [SerializeField] private Text dialogueText;  // 对话显示文本 - 显示NPC的回复内容
    [SerializeField] private Button sendButton;  // 发送按钮 - 点击后发送对话请求

    [Header("参数设置")]
    [SerializeField] private float typingSpeed = 0.05f;  // 打字效果速度 - 每个字符显示的时间间隔(秒)
    [SerializeField] private bool clearInputAfterSend = true;  // 发送后是否清空输入框

    private string characterName; // 存储NPC角色名称
    private bool isWaitingResponse = false; // 标记是否正在等待API响应

    /// <summary>
    /// 初始化方法 - 在游戏开始时执行一次
    /// </summary>
    void Start()
    {
        // 从对话管理器中获取NPC角色名称
        characterName = deepSeekLink.npcCharacter.name;

        // 注册按钮点击事件 - 当按钮被点击时调用SendPlayerMessage方法
        sendButton.onClick.AddListener(SendPlayerMessage);

        // 注册输入框提交事件 - 当在输入框中按回车时也发送消息
        inputField.onSubmit.AddListener((text) => SendPlayerMessage());

        // 初始状态设置:禁用发送按钮,等待玩家输入
        UpdateUIState();
    }

    /// <summary>
    /// 每帧更新调用 - 用于实时更新UI状态
    /// </summary>
    void Update()
    {
        // 动态更新发送按钮状态:有输入内容且不等待响应时才可点击
        sendButton.interactable = !isWaitingResponse && !string.IsNullOrEmpty(inputField.text.Trim());
    }

    /// <summary>
    /// 发送玩家消息 - 按钮点击或回车时调用
    /// </summary>
    public void SendPlayerMessage()
    {
        // 获取输入框内容并去除首尾空格
        string playerMessage = inputField.text.Trim();

        // 检查消息是否为空
        if (string.IsNullOrEmpty(playerMessage))
        {
            Debug.LogWarning("输入内容为空,无法发送");
            return;
        }

        // 检查是否正在等待上一个响应
        if (isWaitingResponse)
        {
            Debug.LogWarning("正在等待AI响应,请稍后再试");
            return;
        }

        // 在对话文本中显示玩家消息
        dialogueText.text = $"玩家: {playerMessage}\n\n{characterName}: 思考中...";

        // 设置等待响应状态
        isWaitingResponse = true;

        // 更新UI状态
        UpdateUIState();

        // 发送对话请求到DeepSeek API
        deepSeekLink.SendDialogueRequest(playerMessage, HandleAIResponse);

        // 发送后清空输入框(如果设置了该选项)
        if (clearInputAfterSend)
        {
            inputField.text = "";
        }
    }

    /// <summary>
    /// 处理AI回复的回调函数 - 在收到DeepSeek API响应后调用
    /// </summary>
    /// <param name="response">AI回复的文本内容</param>
    /// <param name="success">请求是否成功</param>
    private void HandleAIResponse(string response, bool success)
    {
        // 重置等待状态
        isWaitingResponse = false;

        // 更新UI状态
        UpdateUIState();

        // 根据成功状态显示不同的内容
        string displayText = success ?
            $"{characterName}: {response}" :  // 成功:显示NPC回复
            $"{characterName}: (通讯中断,请稍后重试)";  // 失败:显示错误信息

        // 启动打字机效果协程,逐字显示文本
        StartCoroutine(TypewriterEffect(displayText));
    }

    /// <summary>
    /// 打字机效果协程 - 逐字显示文本,创造打字动画效果
    /// </summary>
    /// <param name="text">要显示的完整文本</param>
    private IEnumerator TypewriterEffect(string text)
    {
        // 保存当前对话文本,避免在打字过程中被覆盖
        string currentDialogue = dialogueText.text.Split('\n')[0] + "\n\n";

        // 初始化当前显示的文本
        string currentText = "";

        // 遍历文本中的每个字符
        foreach (char c in text)
        {
            // 将当前字符添加到显示文本中
            currentText += c;

            // 更新对话文本显示(保留玩家消息 + 新的NPC回复)
            dialogueText.text = currentDialogue + currentText;

            // 等待指定时间,控制打字速度
            yield return new WaitForSeconds(typingSpeed);
        }
    }

    /// <summary>
    /// 更新UI状态 - 根据当前状态启用或禁用UI元素
    /// </summary>
    private void UpdateUIState()
    {
        // 等待响应时禁用输入框,响应完成后启用
        inputField.interactable = !isWaitingResponse;

        // 按钮状态在Update中动态更新,这里只处理极端情况
        if (isWaitingResponse)
        {
            sendButton.interactable = false;
        }
    }

    /// <summary>
    /// 清空对话记录 - 可用于开始新的对话
    /// </summary>
    public void ClearDialogue()
    {
        dialogueText.text = "";
        inputField.text = "";
        isWaitingResponse = false;
        UpdateUIState();
    }

    /// <summary>
    /// 设置打字速度 - 可在运行时调整
    /// </summary>
    /// <param name="speed">新的打字速度(秒/字符)</param>
    public void SetTypingSpeed(float speed)
    {
        typingSpeed = Mathf.Clamp(speed, 0.01f, 0.2f); // 限制速度在合理范围内
    }

    /// <summary>
    /// 当脚本被禁用或对象被销毁时调用 - 清理事件监听
    /// </summary>
    void OnDestroy()
    {
        // 移除按钮点击事件监听,防止内存泄漏
        if (sendButton != null)
            sendButton.onClick.RemoveListener(SendPlayerMessage);
    }
}

(2)添加UI控件

  1. 创建一个InputField控件用于接收用户输入
  2. 创建一个Scroll View控件,为其子物体Content添加Text组件来展示AI回复内容
  3. 创建一个Button控件用于触发消息发送功能

将脚本NPCInteraction脚本放置在Canvas下,如下配置脚本变量。

3 测试

运行,在InputField输入框中,输入你好,你是谁?随后回车或者点击发送,都可以将消息发送出去,并且输入框内清空内容,Button处于非激活状态。

———————————————————————————————————————————

4 补档(新增AI具备上下文能力

4.1 核心修改思路

  • 存储历史:在DeepSeekLink脚本中,增加一个List<Message>来记录整个对话过程。
  • 发送历史:在每次构建API请求时,将存储的历史消息列表与当前用户的新消息合并,一起发送。
  • 追加新内容:收到AI回复后,将本轮的用户消息和AI回复都追加到历史列表中。

4.2 具体代码修改

4.2.1 添加历史记录变量

DeepSeekLink类的顶部,npcCharacter 变量下面,添加一个用于存储对话历史的列表。

public class DeepSeekLink : MonoBehaviour
{
    // ... 原有的 [Header] 和变量声明 ...

    [SerializeField] public NPCCharacter npcCharacter;
    // --- 新增:对话历史记录,用于存储整个对话过程的列表 ---
    // 
    private List<Message> conversationHistory = new List<Message>();
    // --- 新增结束 ---

    // ......
}

4.2.2 修改核心请求逻辑

找到 ProcessDialogueRequest 协程,修改其内部构建 messages 列表的方式。不再每次都只发系统提示和当前消息,而是基于历史记录来构建

(可以直接用下面的代码直接替换ProcessDialogueRequest 中的内容)

private IEnumerator ProcessDialogueRequest(string userInput, DialogueCallback callback)
{
    // --- 修改从这里开始 ---
    // 1. 构建本次要发送的消息列表
    List<Message> messagesToSend = new List<Message>();

    // 2. 如果历史记录为空,或者是新对话的开始,需要先加入系统设定
    // 注意:系统消息通常只需要在第一次对话时发送,但为了确保AI始终记得角色,每次都发送也可以。
    // 这里采用每次都发送系统消息的策略,让AI时刻牢记角色。
    messagesToSend.Add(new Message { role = "system", content = npcCharacter.personalityPrompt });

    // 3. 将之前所有的对话历史(不包含系统消息,因为上面已经单独加了)添加到要发送的列表中
    // 注意:conversationHistory 存储的是用户和助手交替的完整对话,我们把它原样加进去
    messagesToSend.AddRange(conversationHistory);

    // 4. 最后,将用户当前的新消息加入列表
    messagesToSend.Add(new Message { role = "user", content = userInput });

    // 5. 构建API请求体,使用组装好的 messagesToSend
    ChatRequest requestBody = new ChatRequest
    {
        model = modelName,
        messages = messagesToSend, // 使用包含上下文的列表
        temperature = temperature,
        max_tokens = maxTokens
    };
    // --- 修改结束 ---

    string jsonBody = JsonUtility.ToJson(requestBody);
    Debug.Log("Sending JSON: " + jsonBody);

    // ... 发送网络请求的代码保持不变 ...
    UnityWebRequest request = CreateWebRequest(jsonBody);
    yield return request.SendWebRequest();

    // ... 错误检查代码保持不变 ...
    if (IsRequestError(request))
    {
        Debug.LogError($"API Error: {request.responseCode}\n{request.downloadHandler.text}");
        callback?.Invoke(null, false);
        yield break;
    }

    // 解析响应
    DeepSeekResponse response = ParseResponse(request.downloadHandler.text);
    if (response != null && response.choices.Length > 0)
    {
        string npcReply = response.choices[0].message.content;

        // --- 新增:将本轮对话存入历史记录 ---
        // 先存用户消息
        conversationHistory.Add(new Message { role = "user", content = userInput });
        // 再存AI回复
        conversationHistory.Add(new Message { role = "assistant", content = npcReply });
        // --- 新增结束 ---

        callback?.Invoke(npcReply, true);
    }
    else
    {
        callback?.Invoke("Unity-YuZhang(陷入沉默)", false);
        // 注意:如果请求失败,不应该将本轮对话存入历史
    }
}

4.2.3 添加清空历史的方法

为了方便开始新对话,可以在 DeepSeekLink 类中添加一个公共方法来清空历史记录。

/// <summary>
/// 清空当前对话历史,开始全新的对话
/// </summary>
public void ClearConversationHistory()
{
    conversationHistory.Clear();
    Debug.Log("对话历史已清空");
}

在 NPCInteraction 脚本中,新增clearButton,需要开始新对话时调用DeepSeekLink的ClearConversationHistory方法。

仅需新增即可,其余代码不用动。

public class NPCInteraction : MonoBehaviour
{
    // ... 原有的 [Header] 和变量声明 ...

    // --- 新增:清除历史记录按钮 ---
    [SerializeField] private Button clearButton;  // 清除历史记录按钮 - 点击后发送请求
    // --- 新增结束 ---

    void Start()
    {
        // ... 原有的代码 ...

        // --- 新增:调用清除历史功能 ---
        // 清除历史记录按钮点击事件 - 当按钮被点击时调用deepSeekLink.ClearConversationHistory方法用于清除原来的历史记录
        clearButton.onClick.AddListener(deepSeekLink.ClearConversationHistory);
        // --- 新增结束 ---
        
        // …………
}

在UI界面,添加一个新的Button,并拖至clearButton的引用即可。

现在运行,连续提问,就会发现AI能够记住之前聊过的内容了。

重要提示:Token消耗,随着对话进行,发送的历史消息会越来越长,消耗的Token数量也会线性增加。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐