使用 C++ 實現一個簡單的 Coding Agent: 簡單的 chat 程式
在 使用 C++ 實現一個簡單的 Coding Agent: 從 curl 呼叫開始 一文中,我們說明了如何透過 curl 去做出簡單的 chat 功能,而在 使用 C++ 實現一個簡單的 Coding Agent: CMake 專案的建立 一文中我們知道如何使用 CMake 建立 C++ 的專案,現在是時候用 C++ 寫一個簡單的聊天系統了。
main.cpp : 程式的進入點
在寫 C/C++ 程式時,我們都會寫一個 main.cpp 作為程式的進入點。
那們我們要怎樣開始呢?從 使用 C++ 實現一個簡單的 Coding Agent: 從 curl 呼叫開始 一文中我們知道了我們會需要將我們的問題提供給 LLM, 接著收到後接收使用者輸入,再送過去給 LLM ,這樣一直來往直到使用者想要離開為止,所以我們可以規劃程式的運作大概像這樣的結構:
sequenceDiagram
participant 使用者
participant CLI 主程式
participant 代理
Note over CLI 主程式,代理: 程式初始化
CLI 主程式->>代理: 建立代理 agent(url, key, model_name)
CLI 主程式-->>使用者: 顯示 "Simple Agent - Enter your query (or 'quit' to exit):"
loop 主迴圈
CLI 主程式-->>使用者: 顯示「User > 」
使用者->>CLI 主程式: 輸入查詢
break 當查詢為 "quit"、"/quit"、"exit" 或 "/exit" 時
CLI 主程式-->>使用者: 離開程式
end
alt 空白輸入
CLI 主程式->>CLI 主程式: 如果 (query.empty())
CLI 主程式-->>CLI 主程式: 繼續(新提示)
else 有效查詢
CLI 主程式->>代理: 結果 = agent.Run(query)
代理-->>CLI 主程式: 回傳回應
CLI 主程式-->>使用者: 輸出「AI > 」+ 結果
end
end
其中 Agent 是我們和 LLM 溝通的實作,我們會將他抽到 agent.cpp 裡面,因此在主程式是不需要介意他的存在,只要知道每一次收到資料,都會透過 agent.Run 去讓 agent 將資料送到遠端就好。
注意:
我們是 chat (聊天), 聊天就是要有來有往,不可以來來來或是往往往,因此我們會等 agent.Run 完成後才收 user 的輸入,所謂的 Agent Loop 就是這樣的概念
端點與模組指定
在開始聊天程式撰寫之前,我們需要獲得 LLM 端點的 URL 以及 KEY 和 MODEL ,因此我們會先這樣用環境變數來指定這些資訊,而不是寫死,這是為了方便切換模組或是端點進行驗證
其中 API_URL 通常會是 http://URL/v1 的格式,但是實際上使用 chat 時候,目標端點會是 http://URL/v1/chat/completions ,所以我們會做一點轉換
const char* api_url = std::getenv("API_URL");
const char* api_key = std::getenv("API_KEY");
const char* model = std::getenv("MODEL");
if (!api_url || !api_key || !model) {
std::cerr << "Please set API_URL, API_KEY, and MODEL environment variables"
<< std::endl;
return -1;
}
std::string url, key, model_name;
url = std::string(api_url) + "/chat/completions";
key = api_key;
model_name = model;這樣我們最後執行程式時,就可以這樣提供端點的資訊,這邊以 openrouter 並使用 openrouter/free 這個免費的模型作為範例:
API_KEY="$OPENROUTER_API_KEY" \
API_URL="https://openrouter.ai/api/v1" \
MODEL="openrouter/free" ./build/src/simple_agentAgent Loop
知道了 API_URL, API_KEY, MODEL 後,我們會將它傳給 Agent 這個物件,接著著使用一個無窮的 while-loop 來跑我們的程式,終止條件就是有人輸入了 quit 等字眼
Agent agent(url, key, model_name);
std::cout << "Simple Agent - Enter your query (or 'quit' to exit):"
<< std::endl;
while (true) {
std::cout << "\nUser > ";
std::string query;
std::getline(std::cin, query);
if (query == "quit" || query == "/quit" || query == "exit" ||
query == "/exit") {
break;
}
if (query.empty()) {
continue;
}
const std::string result = agent.Run(query);
std::cout << "\nAI > " << result << std::endl;
}記得嗎? 對話就是有來有往 ,所以我們每送出去一句話,就會等待 agent.Run 也就是等對方將訊息送回來。
agent.cpp : Agent 的處理
那我們要怎樣實作這個 Agent 呢,首先因為我們在 main.cpp 裡面是這樣呼叫 Agent 的
Agent agent(url, key, model_name);所以很理所當然 Agent 在建構子需要將這些東西存成變數自己使用
Agent::Agent(const std::string& api_url,
const std::string& api_key,
const std::string& model)
: api_url_(api_url), api_key_(api_key), model_(model) {}
Agent::~Agent() = default;資料的送與收
在 使用 C++ 實現一個簡單的 Coding Agent: 從 curl 呼叫開始 一文我們是使用 curl 來進行資料的傳送,而在 C++ 上面我可以使用 libcurl 這個 curl 的函式庫來做出一樣的事情
我們先來講怎麼送資料,由於我們都是要送 json 資料過去,因此這個函式的參數會是一個 json 物件,而該物件在 message.h 裡面我們是這樣定義的
using Json = nlohmann::json;所以我們送資料的方式,原本 shell 下面這樣的呼叫
curl https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '<JSON_DATA>'就會變成將 curl 變成這樣的 C++ 呼叫,注意到因為 curl 是使用 callback 接收 data 的,因此我們等等還要實作 WriteCallback 函式才行,當 callback 完成後,會將資料寫到 resposne 這個字串內,然後我們再返回給呼叫者
(為了版面好看,這邊省略掉一些錯誤處理機制)
Json Agent::SendRequest(const Json& payload) {
CURL* curl = curl_easy_init();
std::string response;
struct curl_slist* headers = nullptr;
headers = curl_slist_append(headers, "Content-Type: application/json");
headers = curl_slist_append(headers, "Accept: application/json");
const std::string auth_header = "Authorization: Bearer " + api_key_;
headers = curl_slist_append(headers, auth_header.c_str());
const std::string request_body = payload.dump();
curl_easy_setopt(curl, CURLOPT_URL, api_url_.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDS, request_body.c_str());
curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE,
static_cast<long>(request_body.size()));
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
// 注意: 這邊指定了接收資料用的 callback, 資料會寫回到 response 變數
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback);
curl_easy_setopt(curl, CURLOPT_WRITEDATA, &response);
const CURLcode res = curl_easy_perform(curl);
curl_slist_free_all(headers);
curl_easy_cleanup(curl);
return Json::parse(response); // 將 response 回傳成 Json 物件
}那麼 WriteCallback 是怎樣實作呢?這邊就要參考 https://curl.se/libcurl/c/CURLOPT_WRITEFUNCTION.html ,我們可以看到原型是這樣的
#include <curl/curl.h>
size_t write_callback(char *ptr, size_t size, size_t nmemb, void *userdata);
CURLcode curl_easy_setopt(CURL *handle, CURLOPT_WRITEFUNCTION, write_callback);所以對應的 C++ 版本就是這樣寫:
size_t Agent::WriteCallback(void* contents,
size_t size,
size_t nmemb,
void* userp) {
const size_t realsize = size * nmemb;
std::string* buffer = static_cast<std::string*>(userp);
buffer->append(static_cast<char*>(contents), realsize);
return realsize;
}Agent 的 Run
那麼 Agent 的 Run 還要做什麼事呢?還記得我們之前在 使用 C++ 實現一個簡單的 Coding Agent: 從 curl 呼叫開始 用了大量的 jq 命令來解析 json 嗎? 在這邊, agent.Run 就是要做類似的事情,將使用這輸入變成 json 格式,並將收到的資料找出 .choices[0].message.content 的地方將其撈出來
因此我們要實作這樣的 curl 命令的組合
curl https://openrouter.ai/api/v1/chat/completions \
-H "Authorization: Bearer $OPENROUTER_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openrouter/free",
"messages": [
{
"role": "user",
"content": "Hi, I am jimmy"
}
]
}' | jq -r '.choices[0].message.content'記得在最後,我們要把之前的資料一起餵出去,這樣 AI 才會有記憶
std::string Agent::Run(const std::string& query,
const std::string& system_prompt) {
Message user_msg;
user_msg.role = Role::kUser;
user_msg.content_type = ContentType::kText;
user_msg.text = query;
messages_.push_back(user_msg);
Json payload;
payload["model"] = model_;
payload["max_tokens"] = 1024;
Json msgs = Json::array();
for (const auto& msg : messages_)
msgs.push_back(MessageToJson(msg));
payload["messages"] = msgs;
const Json response = SendRequest(payload);
if (response.contains("error"))
return "Error: " + response["error"].dump();
std::string content;
if (response.contains("choices") && !response["choices"].empty()) {
const auto& msg = response["choices"][0]["message"];
if (msg.contains("content") && msg["content"].is_string()) {
content = msg["content"].get<std::string>();
} else {
return "Error: Response content is not a string";
}
} else if (response.contains("content")) {
if (response["content"].is_string()) {
content = response["content"].get<std::string>();
} else {
return "Error: Unexpected content format";
}
} else {
return "Error: Invalid response format: " + response.dump();
}
// update messages with assistant response so that the next query
// will have the full conversation history
Message assistant_msg;
assistant_msg.role = Role::kAssistant;
assistant_msg.content_type = ContentType::kText;
assistant_msg.text = content;
messages_.push_back(assistant_msg);
return content;
}在這邊,我們使用到寫在 messages.cpp 裡面一個函式,目的也是為了讓資訊簡化成 json 比較容易用
std::string RoleToString(Role r) {
switch (r) {
case Role::kUser:
return "user";
case Role::kAssistant:
return "assistant";
case Role::kSystem:
return "system";
}
return "user";
}
Json MessageToJson(const Message& msg) {
Json j;
j["role"] = RoleToString(msg.role);
j["content"] = msg.text;
return j;
}Role 分成 User, System, Assistant 這三種,其中 User 就是使用者, Assistant 則是 AI 本身, System 則是代表 System Prompt, 也就是我們怎們引導 AI 思考的基本 prompt, 因為這篇文章還用不到,所以我們先省略掉 system prompt 的部分
執行與測試
程式完成後,我們可以這樣編譯並進行測試
cmake -B build -S . # 告訴 cmake 我們要建立 build 資料夾用來做編譯設定,編譯專案是當前資料夾
cmake --build build # 進行編譯這邊提供除了 openrouter 外,使用 ollama , lm-studio 的方式,如果是本地模型的話,可以使用 gemma:e4b ,效果不會太差
openrouter
openrouter 使用 openrouter/free 來進行測試,記得你要先設定好 OPENROUTER_API_KEY 才可以順利執行
API_KEY="$OPENROUTER_API_KEY" \
API_URL="https://openrouter.ai/api/v1" \
MODEL="openrouter/free" ./build/src/simple_agentollama
假設你要跑本地模型,並且你已經架設好 ollama 的話,則這樣,不過 APIKEY 需要依照你自己的需求進行修改
API_KEY="ollama" \
API_URL="http://localhost:11434/v1" \
MODEL="gemma4:e4b" ./build/src/simple_agentlm-studio
如果你使用的是 lm-studio 的話,改成這樣執行,其中 APIKEY 需要依照你自己的需求進行修改
API_KEY="lmstudio" \
API_URL="http://localhost:1234/v1" \
MODEL="gemma4:e4b" ./build/src/simple_agent範例程式碼
本文的程式碼已經上線,你可以用以下命令取得,對應的 tag 為 v0.2.0, happy coding :)
git clone https://github.com/coldnew/simple-agent.git
cd simple-agent
git checkout v0.2.0展示
本文的成品展示如下: