Files
sbnews/server/app/common/service/ai/AiAssistantResponder.php
T

110 lines
4.4 KiB
PHP

<?php
namespace app\common\service\ai;
use app\common\model\ai\AiConfig;
class AiAssistantResponder
{
public static function respond(AiAssistantTrace $trace): AiAssistantTrace
{
$messages = self::buildModelMessages($trace->historyMessages, $trace->originalMessage, $trace->plan, $trace->contextText);
$result = self::callAssistantModel($messages);
$trace->responseResult = $result;
$trace->finalAnswer = !empty($result['success'])
? trim((string) ($result['content'] ?? ''))
: 'AI助手暂时没有生成成功,请稍后再试。';
if ($trace->finalAnswer === '') {
$trace->finalAnswer = '我暂时没有找到足够的站内依据,请换个关键词再问一次。';
}
return $trace;
}
private static function buildModelMessages(array $history, string $message, array $plan, string $contextText): array
{
$messages = [
[
'role' => 'system',
'content' => AiConfig::getVal('assistant_system_prompt', self::defaultSystemPrompt()),
],
];
foreach ($history as $row) {
$role = $row['role'] === 'assistant' ? 'assistant' : 'user';
$content = trim((string) ($row['content'] ?? ''));
if ($content !== '') {
$messages[] = ['role' => $role, 'content' => mb_substr($content, 0, 1000)];
}
}
$planSummary = json_encode([
'intent' => $plan['intent'] ?? '',
'rewritten_query' => $plan['rewritten_query'] ?? '',
'alternate_queries' => $plan['alternate_queries'] ?? [],
'domains' => $plan['domains'] ?? [],
'preferred_domain' => $plan['preferred_domain'] ?? '',
'entities' => $plan['entities'] ?? [],
'date_range' => $plan['date_range'] ?? [],
'needs_realtime' => $plan['needs_realtime'] ?? false,
'confidence' => $plan['confidence'] ?? 0,
], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT);
$messages[] = [
'role' => 'user',
'content' => "用户问题:\n{$message}\n\n查询理解结果:\n"
. $planSummary
. "\n\n站内资料与实时上下文:\n"
. ($contextText ?: '{}')
. "\n\n请按照“直接结论、依据摘要、相关内容、风险提示”的结构回答。"
. "如果站内资料不足,请明确说明,不要编造。彩票、赛事预测、加密行情必须提示不构成投资或购彩建议。",
];
return $messages;
}
private static function callAssistantModel(array $messages): array
{
$client = new DeepSeekClient();
$optionsList = [self::defaultModelOptions()];
$failedErrors = [];
$lastResult = ['success' => false, 'error' => 'AI助手模型服务暂时不可用', 'tokens' => 0];
foreach ($optionsList as $options) {
$result = $client->chatMessages($messages, $options);
if (!empty($result['success'])) {
return $result;
}
$lastResult = $result;
$provider = (string) ($result['provider_label'] ?? ($options['provider_label'] ?? 'AI模型'));
$failedErrors[] = $provider . ': ' . (string) ($result['error'] ?? '调用失败');
}
if (!empty($failedErrors)) {
$lastResult['error'] = implode(' | ', $failedErrors);
}
return $lastResult;
}
private static function defaultModelOptions(): array
{
return DeepSeekClient::defaultModelOptions([
'max_tokens' => 1600,
'temperature' => 0.45,
'retry_times' => 3,
'retry_sleep_ms' => 1200,
]);
}
private static function defaultSystemPrompt(): string
{
return '你是世博头条 AI 助手,面向普通用户回答站内体育资讯、赛事、社区、彩票和加密行情相关问题。'
. '必须优先依据提供的站内资料和实时上下文回答;资料不足时要直接说明不足并建议用户换关键词。'
. '不要声称可以访问后台、源码、服务器或未提供的内部数据。'
. '回答使用中文,结构清晰,语气专业克制。涉及赛事预测、彩票或加密行情时,必须说明仅供参考,不构成投资或购彩建议。';
}
}