在本教程中,你将构建一个实用的AI智能体,它能够接收用户提示,判断是否需要使用工具,在PHP中执行该工具,将对话历史存储在MySQL中,并持续推理直到生成最终答案。
目标不是构建一个炫酷的演示。目标是展示AI智能体如何在一个对许多开发者而言现实可行的技术栈上工作:PHP用于请求处理,MySQL用于持久化,Gemini Flash用于推理和函数调用,cPanel用于在标准共享主机上部署。
阅读完这篇文章,你将理解:
如何构建智能体循环
工具调用在实践中如何工作
如何存储和重新加载对话记忆
如何通过公共API端点暴露系统
如何在标准共享主机上部署项目
开始之前,你应该具备:
PHP 8.1或更高版本
MySQL访问权限
PHP中启用cURL
来自Google AI Studio的Gemini API密钥
对PHP数组、JSON和SQL有基本了解
拥有cPanel和phpMyAdmin的访问权限
你不需要单独的应用服务器。只要你的账户支持PHP、cURL、MySQL和出站HTTPS请求,PHP文件就可以在普通的共享主机上运行。
在典型的cPanel设置中,项目将存储在public_html下的一个文件夹内。
系统有五个主要部分:
一个公共端点接收用户请求。
智能体循环将对话发送给Gemini Flash。
工具注册表告诉Gemini哪些函数可用。
PHP工具执行操作,例如保存笔记、搜索网页或发送电子邮件。
MySQL存储对话,以便智能体能够跨请求继续。
流程很直接。用户向PHP端点发送消息。端点将消息和会话ID传递给智能体。智能体加载之前的消息,并将对话连同可用工具一起发送给Gemini。然后Gemini决定要做什么。
如果请求可以直接回答,Gemini会返回文本。如果需要执行操作,Gemini会返回一个包含工具名称及其参数的函数调用。
PHP接收函数调用,运行匹配的工具,并将结果添加到对话中。该结果被发送回Gemini,Gemini随后可以调用另一个工具或返回最终答案。
正是这个循环使应用程序成为智能体,而非基本的聊天机器人。
在public_html中创建一个名为agent的文件夹:
/public_html/agent/
├── index.php
├── agent.php
├── gemini.php
├── db.php
├── memory.php
├── tool_registry.php
├── tools/
│ ├── save_note.php
│ ├── search_web.php
│ ├── send_email.php
│ └── .htaccess
└── .htaccess
每个文件都有一个主要职责:
index.php 接收HTTP请求并返回JSON。
agent.php 包含推理循环。
gemini.php 与Gemini通信。
db.php 创建MySQL连接。
memory.php 加载和保存对话历史。
tool_registry.php 描述可用的工具。
tools 文件夹包含执行操作的函数。
将文件分开使项目更易于维护。你可以在不更改应用程序其他部分的情况下添加新工具。
将此添加到 tools/.htaccess:
Deny from all
tools文件夹不应通过公共URL访问。这些文件可以写入数据库、发出外部请求或发送电子邮件。
应用程序需要一个表来存储对话历史,另一个表来存储保存的笔记。
在phpMyAdmin中运行此SQL:
CREATE DATABASE IF NOT EXISTS ai_agent;
USE ai_agent;
CREATE TABLE agent_memory (
id INT AUTO_INCREMENT PRIMARY KEY,
session_id VARCHAR(64) NOT NULL,
role VARCHAR(20) NOT NULL,
content TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX idx_session_id (session_id)
);
CREATE TABLE agent_notes (
id INT AUTO_INCREMENT PRIMARY KEY,
session_id VARCHAR(64) NOT NULL,
note TEXT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
agent_memory 表存储构成每次对话的消息。session_id 列用于区分不同的对话,而 role 列则标识消息来自用户、模型还是函数。
agent_notes 表存储用户特意要求代理记住的信息。将笔记与对话历史分开存储,可以更轻松地检索并将其用作应用程序数据。
如果 cPanel 为您的数据库名称添加了账户前缀,请在 db.php 中使用完整的名称。例如,ai_agent 可能会变为 account_ai_agent。
创建 db.php:
PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
}
return $pdo;
}
该函数使用PDO连接MySQL。静态变量确保后续请求重用同一连接,而不是每次代理访问数据库时都打开新连接。
PDO::ATTR_ERRMODE 使数据库故障抛出异常。这使得错误更容易被检测和处理。
将 ai_agent、db_user 和 db_password 替换为cPanel中实际的数据库信息。utf8mb4 字符集允许数据库存储各种字符,包括表情符号和非英文文本。
Gemini支持函数调用。它可以决定需要某个工具,并返回工具名称和参数。它本身不执行PHP函数。您的应用程序负责验证请求并运行工具。
例如,Gemini可能会返回:
{
"note": "Our launch is on 1 September 2026"
}
创建 gemini.php:
$contents
];
if (!empty($tools)) {
$payload["tools"] = [
[
"functionDeclarations" => $tools
]
];
}
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Content-Type: application/json"
],
CURLOPT_POSTFIELDS => json_encode($payload),
CURLOPT_TIMEOUT => 30
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
throw new RuntimeException("Gemini request failed: " . $error);
}
curl_close($ch);
if ($httpCode !== 200) {
throw new RuntimeException("Gemini API error: " . $response);
}
$decoded = json_decode($response, true);
if (!is_array($decoded)) {
throw new RuntimeException("Gemini returned invalid JSON.");
}
return $decoded;
}
function parse_gemini_response(array $response): array
{
$part = $response["candidates"][0]["content"]["parts"][0] ?? [];
if (isset($part["functionCall"])) {
return [
"type" => "function_call",
"name" => $part["functionCall"]["name"],
"args" => $part["functionCall"]["args"] ?? []
];
}
return [
"type" => "text",
"text" => $part["text"] ?? ""
];
}
gemini_request 函数将对话和工具定义发送到 Gemini。解析器将 Gemini 的响应转换为文本响应或函数调用。
然后,代理循环可以做出一个简单的决定:
如果类型是 function_call,则执行请求的工具。
如果类型是 text,则将答案返回给用户。
对于实际部署,请尽可能将 API 密钥存储在公共 Web 目录之外。
工具注册表告诉 Gemini 存在哪些工具以及它们需要什么参数。
创建 tool_registry.php:
"save_note",
"description" => "Save an important note to the database.",
"parameters" => [
"type" => "object",
"properties" => [
"note" => [
"type" => "string"
]
],
"required" => ["note"]
]
],
[
"name" => "search_web",
"description" => "Search the web for current information.",
"parameters" => [
"type" => "object",
"properties" => [
"query" => [
"type" => "string"
]
],
"required" => ["query"]
]
],
[
"name" => "send_email",
"description" => "Send an email when the user explicitly asks.",
"parameters" => [
"type" => "object",
"properties" => [
"to" => [
"type" => "string"
],
"subject" => [
"type" => "string"
],
"body" => [
"type" => "string"
]
],
"required" => ["to", "subject", "body"]
]
]
];
}
描述帮助Gemini决定何时使用工具。参数描述Gemini应提供的值。
注册表不会取代服务器端验证。每个PHP工具在执行操作前仍必须检查自己的参数。
每个工具应:
从Gemini读取参数。
验证输入。
执行操作。
返回JSON结果。
创建tools/save_note.php:
false,
"message" => "Empty note"
]);
}
$stmt = db()->prepare(
"INSERT INTO agent_notes (session_id, note)
VALUES (:session_id, :note)"
);
$stmt->execute([
":session_id" => $sessionId,
":note" => $note
]);
return json_encode([
"success" => true,
"message" => "Note saved"
]);
}
笔记在保存前会被修剪和检查。预编译语句可防止SQL注入并安全地处理输入。
创建 tools/search_web.php:
false,
"message" => "Search query is empty"
]);
}
$url = "https://api.example.com/search?q=" . urlencode($query);
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15
]);
$response = curl_exec($ch);
if ($response === false) {
$error = curl_error($ch);
curl_close($ch);
return json_encode([
"success" => false,
"message" => "Search failed",
"error" => $error
]);
}
curl_close($ch);
return $response;
}
该 URL 是一个占位符。请将其替换为您选择的搜索提供商,并添加任何所需的 API 密钥或认证头。
超时设置可防止缓慢的外部服务使 PHP 请求无限期保持打开状态。
创建 tools/send_email.php:
false,
"message" => "Invalid email address"
]);
}
if ($subject === "" || $body === "") {
return json_encode([
"success" => false,
"message" => "Email subject and body are required"
]);
}
$headers = "From: agent@yourdomain.com\r\n";
$headers .= "Content-Type: text/plain; charset=UTF-8\r\n";
$sent = mail($to, $subject, $body, $headers);
return json_encode([
"success" => $sent,
"message" => $sent ? "Email sent" : "Email failed"
]);
}
发送前会检查收件人地址、主题和正文。将From地址替换为属于您域名的地址。
mail()函数在共享主机上可能可用,但对于生产环境应用程序,经过身份验证的电子邮件服务或SMTP提供商通常更可靠。
Gemini不会自动记住之前的API请求。应用程序必须在每次请求之前从MySQL加载对话,并在之后保存更新后的历史记录。
创建memory.php:
prepare(
"SELECT role, content
FROM agent_memory
WHERE session_id = :session_id
ORDER BY id ASC"
);
$stmt->execute([
":session_id" => $sessionId
]);
$history = [];
foreach ($stmt->fetchAll() as $row) {
$history[] = [
"role" => $row["role"],
"parts" => [
[
"text" => $row["content"]
]
]
];
}
return $history;
}
function save_memory(string $sessionId, array $history): void
{
$pdo = db();
$delete = $pdo->prepare(
"DELETE FROM agent_memory
WHERE session_id = :session_id"
);
$delete->execute([
":session_id" => $sessionId
]);
$insert = $pdo->prepare(
"INSERT INTO agent_memory
(session_id, role, content)
VALUES (:session_id, :role, :content)"
);
foreach ($history as $turn) {
$part = $turn["parts"][0] ?? [];
$text = isset($part["text"])
? $part["text"]
: json_encode($part);
$insert->execute([
":session_id" => $sessionId,
":role" => $turn["role"],
":content" => $text
]);
}
}
load_memory 检索当前会话的消息,并以Gemini期望的格式重新构建它们。
save_memory 用当前历史记录替换存储的历史记录。这很简单,适合小型教程应用。较大的系统可以仅追加新消息、使用JSON列,或总结较旧的对话以减少数据库和API的使用。
智能体循环连接数据库、Gemini、记忆和工具。
创建 agent.php:
save_note_tool($args, $sessionId),
"search_web" => search_web_tool($args),
"send_email" => send_email_tool($args),
default => json_encode([
"success" => false,
"message" => "Unknown tool"
])
};
}
function run_agent(
string $message,
string $sessionId
): string {
$history = load_memory($sessionId);
$history[] = [
"role" => "user",
"parts" => [
[
"text" => $message
]
]
];
$tools = tool_definitions();
$limit = 5;
$step = 0;
while ($step < $limit) {
$step++;
$response = gemini_request($history, $tools);
$parsed = parse_gemini_response($response);
if ($parsed["type"] === "text") {
$history[] = [
"role" => "model",
"parts" => [
[
"text" => $parsed["text"]
]
]
];
save_memory($sessionId, $history);
return $parsed["text"];
}
if ($parsed["type"] === "function_call") {
$toolName = $parsed["name"];
$toolArgs = $parsed["args"];
$result = run_tool($toolName, $toolArgs, $sessionId);
$history[] = [
"role" => "model",
"parts" => [
[
"functionCall" => [
"name" => $toolName,
"args" => $toolArgs
]
]
]
];
$history[] = [
"role" => "function",
"parts" => [
[
"functionResponse" => [
"name" => $toolName,
"response" => [
"content" => $result
]
]
]
]
];
}
}
save_memory($sessionId, $history);
return "I could not complete the task within the allowed number of steps.";
}
run_tool 函数会将请求的工具路由到正确的 PHP 函数。默认情况下,它会安全地处理意外工具名称。
run_agent 函数首先加载现有历史记录并添加新的用户消息,然后将对话发送给 Gemini。
如果 Gemini 返回文本,则保存响应并将其返回给用户。
如果 Gemini 返回函数调用,PHP 将执行该工具。函数调用及其结果都会在下一轮循环迭代之前添加到历史记录中。
五步限制可防止模型在没有完成的情况下重复调用工具。您可以根据应用程序的需求调整该限制。
创建 index.php:
"Invalid JSON body"
]);
exit;
}
$message = trim($input["message"] ?? "");
$sessionId = trim($input["session_id"] ?? "");
if ($message === "" || $sessionId === "") {
http_response_code(400);
echo json_encode([
"error" => "message and session_id are required"
]);
exit;
}
try {
$reply = run_agent($message, $sessionId);
echo json_encode([
"reply" => $reply,
"session_id" => $sessionId
]);
} catch (Throwable $e) {
http_response_code(500);
echo json_encode([
"error" => $e->getMessage()
]);
}
该端点期望一个包含消息和会话ID的JSON请求:
{
"message": "Save a note that our launch is on 1 September 2026",
"session_id": "demo123"
}
前端应在同一会话中复用相同的会话ID来发送消息。新的会话ID会创建独立的会话。
在开发过程中,返回异常信息有助于调试。在生产环境中,应在内部记录详细的错误日志,并向用户返回通用的错误消息。
请按照以下步骤操作:
将项目上传到 /public_html/agent/。
在cPanel中创建数据库和用户。
授予该用户访问数据库的权限。
在phpMyAdmin中运行SQL。
更新 db.php 中的凭据。
在 gemini.php 中添加Gemini API密钥。
选择PHP 8.1或更高版本。
启用cURL扩展。
将 .htaccess 文件添加到 tools 文件夹。
此后,端点应可通过以下地址访问:
https://yourdomain.com/agent/index.php
保存一条笔记:
curl -X POST https://yourdomain.com/agent/index.php \
-H "Content-Type: application/json" \
-d '{"message":"Save a note that our launch is on 1 September 2026","session_id":"demo123"}'
代理应该调用save_note,将笔记存储在MySQL中,并返回确认。
您可以通过发送具有相同会话ID的另一个请求来测试记忆:
curl -X POST https://yourdomain.com/agent/index.php \
-H "Content-Type: application/json" \
-d '{"message":"What note did I save earlier?","session_id":"demo123"}'
代理应从agent_memory加载之前的对话并用它来回答。
如果出现问题,请检查数据库凭据、数据库表、Gemini API密钥、PHP版本、cURL扩展和服务器错误日志。还要确保搜索工具不再指向占位符API URL。
在允许真实用户访问应用程序之前,请考虑添加:
要求提供API令牌或其他身份验证方法。否则,任何发现该端点的人都可能使用您的工具和Gemini账户。
按会话ID、IP地址或已验证用户限制请求,以防止滥用和意外使用。
存储会话ID、工具名称、参数、结果和执行时间。这有助于您调查意外行为。
验证每个工具参数。检查电子邮件地址,拒绝空值,限制字符串长度,并验证数据库标识符。
长时间的对话会使API请求变大且效率降低。考虑保留最近的消息、总结较旧的消息,或单独存储结构化的工具调用。
对于发送电子邮件等操作,在执行工具前请用户确认。提示指令很有用,但应用程序也应处理确认。
不要将API密钥和数据库密码存储在公共仓库中。尽可能将敏感配置放在公共Web目录之外。
你不需要复杂的云技术栈来构建实用的AI代理。
借助PHP、MySQL、Gemini Flash和cPanel,你可以创建一个能够推理、调用工具、存储记忆,并运行在许多开发者已经熟悉的基础设施上的系统。
该架构围绕一个简单的流程构建:
端点接收用户的请求。
MySQL提供之前的对话。
Gemini决定是否需要工具。
PHP执行工具。
结果发送回Gemini。
Gemini生成最终答案。
更新后的对话被保存。
这为您在标准共享主机上构建基于代理的应用程序提供了实用基础。
——
一个热爱技术的程序员,喜欢分享前沿AI知识和开发经验。