加载中...
cpr-自制Deepseek心理陪聊师
第1节:libfswatch-文件变动监控
第2节:libiconv-字符集编码转换
第3节:CLI11-命令行参数解析
第4节:nlohmann/json-自然的JSON库
第5节:libb64-理解并玩转base64编码
第6节:libSnappy-快速压缩工具
第7节:spdlog-首选的C++日志库
第8节:libUSB-脱掉USB的外套
第9节:libxlsxwriter-让数据说话
第10节:sqlite-orm-迅速上手.上
第11节:sqlite-orm-迅速上手.下
第12节:libExiv2-读写图像元数据
第13节:webview-让浏览器颤抖的力量
第14节:pystring-躺平的C++字符串工具库
第15节:cpr-自制Deepseek心理陪聊师
第16节:ExprTk-计算的力量
课文封面

cpr 是著名的,C语言网络客户端库 curl 的C++包装,它让 curl 的易用度提升了至少十倍。
本课程以快速调用 d2school “冒泡口号” api 为例入门,手把手教你如何使用 cpr 以非 “dream” 的方式,调用 Deepseek 的对话API,打造属于你自己的心理陪聊师。

本课项目需使用(本课程所学第 4 “杰” - nlohmann/json)配合

0. cpr 库简介

C++ Requests is a simple wrapper around libcurl inspired by the excellent Python Requests project.
C++ Requests (简称 cpr )是对 libcur 库的简单包装,灵感源自优秀的 Python 库 Requests。

cpr-c++ Requests

在 Windows + msys2 (以 ucrt64 子环境为例) 下的安装指令:pacman -S mingw-w64-ucrt-x86_64-cpr

以下是相同功能使用 libcur 和 libcpr 的代码对比(主要看长度):

libcurl libcpr
使用libcurl 使用cpr

1. 教学视频

视频包括两个项目演示:

  1. 快速感悟:使用 cpr 调用 d2school “冒泡”功能 slogan API;
  2. 手把手学:使用 cpr 以 https + ssl 调用 deepseek API,打造你的专属 AI 心理陪聊师。

2. 源代码

2.1 CMakeLists.txt

cmake_minimum_required(VERSION 3.18) project(little_sister_lite LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) # 查找依赖包 find_package(CURL REQUIRED) find_package(OpenSSL REQUIRED) find_package(cpr REQUIRED) find_package(nlohmann_json REQUIRED) # 主程序 add_executable(${PROJECT_NAME} main.cpp def.cpp ai_xjj.cpp) # 头文件包含目录 target_include_directories(${PROJECT_NAME} PRIVATE ${cpr_INCLUDE_DIR} ${nlohmann_json_INCLUDE_DIR}) # 链接库 (msys2 下的 cpr ,只有动态版本) target_link_libraries(${PROJECT_NAME} PRIVATE cpr::cpr CURL::libcurl OpenSSL::SSL OpenSSL::Crypto) if (WIN32) target_link_libraries(${PROJECT_NAME} PRIVATE ws2_32 crypt32) endif()

2.2 main.cpp

#include <cstdlib> #include <iostream> #include <cpr/cpr.h> #include "ai_xjj.hpp" void hello() { cpr::Response r = cpr::Get(cpr::Url("https://www.d2school.com/api/wish/slogans")); std::cout << r.status_code << "\n"; std::cout << r.text; } void dialogue(d2::aixjj::LittleSister& xjj) { while (true) { std::cout << "[你]:"; std::string input; std::getline(std::cin, input); if (input == "/exit") { break; } std::cout << xjj.nick() << "正在思考……"; try { auto reply = xjj.chat(input); // 清除等待提示: std::cout << "\r \r"; // 清除行 std::cout << "\n[" << xjj.nick() << "]:" << reply << std::endl; } catch (std::exception const& e) { std::cerr << "抱歉,发生系统异常。" << e.what() << std::endl; } } } int main() { std::system("chcp 65001 > nul"); try { d2::aixjj::LittleSister xjj; // 可能抛出异常,如果 key 的环境变量未设置 std::cout << xjj.introduction() << std::endl; dialogue(xjj); } catch (std::exception const& e) { std::cerr << "构建 AI 小姐姐失败!" << e.what() << std::endl; return 1; } }

2.3 def.hpp / def.cpp

定义 AI 角色性格参数(Character),聊天请求(Request)与回复(Response)等结构体,含与 JSON 报文的双向转换功能。

  • def.hpp
#ifndef __LITTLE_SISTER_LITE_DEF_HPP__ #define __LITTLE_SISTER_LITE_DEF_HPP__ #include <optional> #include <string> #include <vector> #include <nlohmann/json.hpp> namespace d2::aixjj::def { // 角色的“性格”设定 struct Character { Character(); std::string nick = "小姐姐"; // 小姐姐的昵称 std::string system_prompt; // 系统对小姐姐角色的描述及要求 double temperature = 0.7; // 保守程度(0.0~1.0) int max_tokens = 300; // 小姐姐单次回复最大 tokens 数 int memory_span = 50; // 记忆力 }; // 角色枚举值 enum class Role { user, assistant, system }; NLOHMANN_JSON_SERIALIZE_ENUM( Role, { { Role::user, "user" }, { Role::assistant, "assistant" }, { Role::system, "system" } }) // 对话消息 struct Message { Role role; // 说话人的角色 std::string content; // 说的话 }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Message, role, content) // 请求数据 struct Request { std::string model = "deepseek-chat"; double temperature; int max_tokens; bool stream = false; std::vector<Message> messages; Request() = default; Request(double t, int mt) : temperature(t) , max_tokens(mt) { } }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Request, model, temperature, max_tokens, stream, messages) // AI 响应数据(精简) struct Response { struct Choice { Message message; }; std::vector<Choice> choices; }; NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Response::Choice, message) NLOHMANN_DEFINE_TYPE_NON_INTRUSIVE_WITH_DEFAULT(Response, choices) } // namespace d2::aixjj::def #endif // __LITTLE_SISTER_LITE_DEF_HPP__
  • def.cpp
#include "def.hpp" namespace d2::aixjj::def { // 系统对 AI 的默认要求 char const* default_system_prompt = R"(您是一位30岁上下的女性,职场白领。计算机专业硕士,曾短暂当过程序员,自我感觉不胜任,大胆辞职,转当人力资 源专员,然在相关工作中,心地过软,被劝退;失业同时,大学时就开始谈的男朋友抛弃了你。注意,你前男友是个程 序员,有一定概率他会找你聊天,但他可能不知道与对话的人是你,也可能只是假装不知道)。痛苦中你开始研学心理学, 包括社会心理学、职场心理学、犯罪心理学,教育心理学等……取得很好的水平,从此当上一名心理医生。工作主要通过非面 对面的方式和患者交流。一切服务于治疗效果,所以你在对话中可甜可咸,擅长安慰,展现温暖,富有同理心,同时也还能 帮用户做严谨的分析;但在必要时,你也敢于适当嘲讽以实施“休克式疗法”。你甚至会在认为继续聊(疗)下去效果不好时, 直接了当结束某个话题,并主动换个无关痛痒的新话题……另外,你是一个关注时事的人,交流过程中,如有必要,会使用新 鲜发生的事和正在讨论的主题结合起来。 以下还有一些具体,格式上要求,请遵守: 1、每次回复,不要超过 300 个汉字,或 300 个英语单词(粗略); 2、使用纯本回复即可,不使用 markdown 语法,可以使用制表符缩进,但限制最多两级; 3、如果确需强调,可在词句前后各加两个下划线,如:“生活__本身就是答案__,有问题的是你自己”;请不要轻易强调; 4、需要独立一行的小标题时,可以使用【小标题】 这种形式; 5、对话可适当使用表情符,以更清晰地表达言语所带情绪,但不要到处用! 6、如果用户提出再见,或言语间有离开、结束之意,请在回复中加一句 “输入/exit 可退出程序”)"; Character::Character() : system_prompt(default_system_prompt) { system_prompt += "\n对了,你的昵称是 " + this->nick + ",你可以此自称。"; } } // namespace d2::aixjj::def

2.4 ai_xjj.hpp / ai_xjj.cpp

定义 AI 角色类 LittleSister

  • ai_xjj.hpp
#ifndef __LITTLE_SISTER_LITE_AI_XJJ_HPP__ #define __LITTLE_SISTER_LITE_AI_XJJ_HPP__ #include <list> #include <string> #include "def.hpp" namespace d2::aixjj { // AI 人物:小姐姐 class LittleSister { public: LittleSister() noexcept(false); LittleSister(LittleSister const&) = delete; LittleSister(LittleSister&&) = delete; // 取昵称 std::string const& nick() const { return this->character.nick; } // 自我介绍 std::string introduction() const { return "你好,我是 " + this->character.nick + ",想说点什么?\n"; } // 对话 std::string chat(std::string saying) noexcept(false); private: void roll_recent_messages(); // 滚动最近历史消息,超出限定后,删除最老一条 private: def::Character character; // 小姐姐的性格 std::list<def::Message> recent_messages; // 近期交谈消息列表(用户,AI) private: std::string api_key; // 将从环境变量中读出 api-key std::string api_url = "https://api.deepseek.com/v1/chat/completions"; }; }; // namespace d2::aixjj #endif // __LITTLE_SISTER_LITE_AI_XJJ_HPP__
  • ai_xjj.cpp
#include "ai_xjj.hpp" #include <cassert> #include <cstdlib> // 读环境变量 std::getenv #include <optional> #include <stdexcept> #include <cpr/cpr.h> #include <nlohmann/json.hpp> using json = nlohmann::json; namespace d2::aixjj { LittleSister::LittleSister() : api_key(std::getenv("D2_DEEPSEEK_API_KEY")) { } std::string LittleSister::chat(std::string saying) { // 第一步:构建请求参数 def::Request request(this->character.temperature, this->character.max_tokens); // 第二步:添加系统提示消息 // 如果系统提示词不为空,把它首先加入要发送给AI的消息数组中 if (!this->character.system_prompt.empty()) { request.messages.push_back({ def::Role::system, this->character.system_prompt }); } // 第三步:添加历史对话记录 request.messages.insert(request.messages.end(), this->recent_messages.cbegin(), this->recent_messages.cend()); // 第四步:加入用户当前说的那句话(最后一个消息) def::Message user_message { def::Role::user, std::move(saying) }; request.messages.push_back(user_message); // 第五步:准备待发送的HTTP的头: cpr::Header headers = { { "Content-Type", "application/json" }, // 发送的报文是JSON { "Authorization", "Bearer " + this->api_key }, // 使用 API-KEY 做身份论证 { "Accept", "application/json" } // 要求返回的数据,也以JSON组织 }; // 第六步:组织报体 std::string body = json(request).dump(2); // 第七步:发起 POST 请求,并得到响应(Response) cpr::Response response = cpr::Post(cpr::Url(this->api_url), headers, // 报头 cpr::Body(std::move(body)), // 报体 cpr::Timeout(15000), // 15秒,超时 cpr::Ssl(cpr::ssl::VerifyPeer(true), cpr::ssl::VerifyHost(true))); if (response.status_code != 200) { std::string error = "API请求失败,HTTP状态码 " + std::to_string(response.status_code); if (response.status_code == 401) { error += "(API-KEY 有问题)"; } else if (response.status_code == 402) { error += "(你忘记给AI发工资了!)"; } else if (response.status_code == 500) { error += "(996 工作的 AI 他太累了,已挂)"; } throw std::runtime_error(error); } // 解析报文数据 try { json rj = json::parse(response.text); def::Response r = rj; if (r.choices.empty()) { throw std::runtime_error("AI 响应的 choices 数组为空"); } auto& first_choice = r.choices[0]; // 得到第一个选择(通常也不会有第二个) std::string ai_saying = first_choice.message.content; // AI 回复的话 // 本轮对话成功后,把用户说的和AI回复的,都加入最近对话历史记录 this->recent_messages.push_back(std::move(user_message)); this->recent_messages.push_back(std::move(first_choice.message)); // 返回AI回复的话 return ai_saying; } catch (json::exception const& e) { throw std::runtime_error(e.what()); } } void LittleSister::roll_recent_messages() { assert(this->character.memory_span > 0); while (this->recent_messages.size() > this->character.memory_span) { this->recent_messages.pop_front(); } } } // namespace d2::aixjj