Agentic AIC++verifiedVerified
ReAct Agent Pattern in C++
Interleaves chain-of-thought Reasoning with Action execution, enabling LLMs to dynamically plan, act, and observe in a loop.
How to Implement the ReAct Agent Pattern in C++
1Step 1: Define the Thought-Action-Observation data structures
struct Observation {
std::string content;
bool isError;
};
using ToolFn = std::function<Observation(const std::string&)>;
using ReasonFn = std::function<std::pair<std::string, std::string>(
const std::string&, const std::vector<std::string>&)>;2Step 2: Implement the ReAct loop: Thought -> Action -> Observation
std::string reactLoop(const std::string& query,
ReasonFn reason,
std::map<std::string, ToolFn>& tools,
int maxSteps = 5) {
std::vector<std::string> history;
history.push_back("Query: " + query);
for (int i = 0; i < maxSteps; ++i) {
// Thought + Action selection
auto [thought, action] = reason(query, history);
history.push_back("Thought: " + thought);
if (action == "FINISH") return thought;
// Execute the action
auto it = tools.find(action);
Observation obs = it != tools.end()
? it->second(thought)
: Observation{"Unknown tool: " + action, true};
history.push_back("Observation: " + obs.content);
}
return "Max steps reached";
}3Step 3: Demonstrate with a simple tool
int main() {
std::map<std::string, ToolFn> tools;
tools["search"] = [](const std::string& q) -> Observation {
return {"Result for: " + q, false};
};
auto answer = reactLoop(
"What is C++20?",
[n = 0](const std::string&,
const std::vector<std::string>& hist) mutable
-> std::pair<std::string, std::string> {
if (++n >= 3) return {"C++20 is a major standard", "FINISH"};
return {"I need to search for this", "search"};
},
tools
);
std::cout << "Answer: " << answer << "\n";
}#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <functional>
#include <memory>
#include <format>
#include <chrono>
#include <stdexcept>
#include <variant>
// [step] Define structured types for the ReAct trace
enum class StepType { Thought, Action, Observation };
struct TraceEntry {
StepType type;
std::string content;
std::chrono::steady_clock::time_point timestamp;
};
struct ToolResult {
std::string output;
bool isError;
std::chrono::milliseconds duration;
};
// [step] Define the Tool interface with RAII registration
class ITool {
public:
virtual ~ITool() = default;
virtual std::string name() const = 0;
virtual std::string description() const = 0;
virtual ToolResult execute(const std::string& input) = 0;
};
struct AgentConfig {
int maxSteps = 10;
std::chrono::seconds timeout{60};
bool verbose = false;
};
// [step] Build the ReAct agent with tool registry, trace, and timeout
class ReActAgent {
AgentConfig config_;
std::map<std::string, std::unique_ptr<ITool>> tools_;
std::vector<TraceEntry> trace_;
using ReasonFn = std::function<std::pair<std::string, std::string>(
const std::string&, const std::vector<TraceEntry>&,
const std::vector<std::string>&)>;
ReasonFn reasoner_;
public:
explicit ReActAgent(AgentConfig config, ReasonFn reasoner)
: config_(std::move(config)), reasoner_(std::move(reasoner)) {}
void registerTool(std::unique_ptr<ITool> tool) {
auto name = tool->name();
tools_.emplace(std::move(name), std::move(tool));
}
std::string run(const std::string& query) {
trace_.clear();
auto start = std::chrono::steady_clock::now();
addTrace(StepType::Observation, "User: " + query);
std::vector<std::string> toolNames;
for (const auto& [name, _] : tools_) toolNames.push_back(name);
for (int i = 0; i < config_.maxSteps; ++i) {
auto elapsed = std::chrono::steady_clock::now() - start;
if (elapsed > config_.timeout)
throw std::runtime_error("Agent timeout exceeded");
auto [thought, action] = reasoner_(query, trace_, toolNames);
addTrace(StepType::Thought, thought);
if (action == "FINISH") {
if (config_.verbose) printTrace();
return thought;
}
addTrace(StepType::Action, action);
auto it = tools_.find(action);
if (it == tools_.end()) {
addTrace(StepType::Observation,
std::format("Error: tool '{}' not found", action));
continue;
}
auto result = it->second->execute(thought);
addTrace(StepType::Observation,
result.isError ? "Error: " + result.output : result.output);
}
if (config_.verbose) printTrace();
throw std::runtime_error("Max reasoning steps exceeded");
}
const std::vector<TraceEntry>& getTrace() const { return trace_; }
private:
void addTrace(StepType type, std::string content) {
trace_.push_back({type, std::move(content),
std::chrono::steady_clock::now()});
}
void printTrace() const {
for (const auto& entry : trace_) {
const char* label = entry.type == StepType::Thought ? "THINK"
: entry.type == StepType::Action ? "ACT"
: "OBS";
std::cout << std::format("[{}] {}\n", label, entry.content);
}
}
};
// [step] Demo with a concrete search tool
class SearchTool : public ITool {
public:
std::string name() const override { return "search"; }
std::string description() const override { return "Web search"; }
ToolResult execute(const std::string& input) override {
auto start = std::chrono::steady_clock::now();
std::string result = "Search result for: " + input;
auto dur = std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - start);
return {result, false, dur};
}
};
int main() {
ReActAgent agent(
{.maxSteps = 5, .verbose = true},
[n = 0](const std::string& query,
const std::vector<TraceEntry>& trace,
const std::vector<std::string>&) mutable
-> std::pair<std::string, std::string> {
if (++n >= 3) return {"C++20 adds concepts and ranges", "FINISH"};
return {"Need more info about " + query, "search"};
}
);
agent.registerTool(std::make_unique<SearchTool>());
std::cout << "Answer: " << agent.run("What is C++20?") << "\n";
}ReAct Agent Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
ReAct Agent Pattern in the Real World
“Like a detective investigating a case: they form a hypothesis (Thought), gather evidence by interviewing witnesses or examining clues (Action), analyze what they found (Observation), and then refine their theory. They keep investigating until they solve the case.”