110 lines
4.4 KiB
PHP
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 助手,面向普通用户回答站内体育资讯、赛事、社区、彩票和加密行情相关问题。'
|
|
. '必须优先依据提供的站内资料和实时上下文回答;资料不足时要直接说明不足并建议用户换关键词。'
|
|
. '不要声称可以访问后台、源码、服务器或未提供的内部数据。'
|
|
. '回答使用中文,结构清晰,语气专业克制。涉及赛事预测、彩票或加密行情时,必须说明仅供参考,不构成投资或购彩建议。';
|
|
}
|
|
}
|