本课项目需使用(本课程所学第 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。

在 Windows + msys2 (以 ucrt64 子环境为例) 下的安装指令:pacman -S mingw-w64-ucrt-x86_64-cpr。
以下是相同功能使用 libcur 和 libcpr 的代码对比(主要看长度):
| libcurl | libcpr |
|---|---|
![]() |
![]() |
1. 教学视频
视频包括两个项目演示:
- 快速感悟:使用 cpr 调用 d2school “冒泡”功能 slogan API;
- 手把手学:使用 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

