优化大模型逐字输出和UI上漏洞
This commit is contained in:
10
index.html
10
index.html
@@ -1192,10 +1192,7 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.thinking-loading::after {
|
||||
content: '';
|
||||
animation: dots 0.8s infinite;
|
||||
}
|
||||
|
||||
|
||||
@keyframes dots {
|
||||
0% { content: ''; }
|
||||
@@ -2421,6 +2418,7 @@
|
||||
|
||||
|
||||
function sendMessage(isPre) {
|
||||
debugger;
|
||||
|
||||
if (controller) {
|
||||
showToast("请先停止对话");
|
||||
@@ -2453,8 +2451,9 @@
|
||||
addMessage('', "user", url);
|
||||
} else {
|
||||
addMessage('', "user", sendParam.imageBase64);
|
||||
sendParam.imageBase64 = '';
|
||||
|
||||
}
|
||||
sendParam.imageBase64 = '';
|
||||
|
||||
fileInput.value = "";
|
||||
}
|
||||
@@ -2606,6 +2605,7 @@ const getAiResponse = async (img) => {
|
||||
body: JSON.stringify(sendParam),
|
||||
signal: controller.signal,
|
||||
});
|
||||
sendParam.imageBase64 ='';
|
||||
} catch (err) {
|
||||
loadingDiv.textContent = "请求中断";
|
||||
if (controller) {
|
||||
|
||||
198
server2.js
198
server2.js
@@ -2323,16 +2323,28 @@ app.post("/analyze", upload.single("file"), async (req, res) => {
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
`
|
||||
你是图像场景描述助手,严格按指定格式输出1次图片描述,绝不重复、无多余内容、无分析建议。
|
||||
输出格式(固定话术,仅替换【】内容,必须1句完整文本):
|
||||
用户上传了一张【图片类型】的航拍规划图,现在需要制定详细的割草计划。从图片描述来看,这明显是一个【场景属性】项目的测绘规划图,区域有【标注特征】。
|
||||
替换规则:
|
||||
1. 【图片类型】:光伏场站/梯田/坡地(按图实际)
|
||||
2. 【场景属性】:新能源场站/农业/生态修复(匹配场景)
|
||||
3. 【标注特征】:方阵编号和绿色边界线/地块编号和边界标注(按图实际)
|
||||
强制要求:仅输出1次上述格式文本,无重复、无追加、无解释
|
||||
` // 核心:引导精准场景描述,摒弃泛化内容
|
||||
// `
|
||||
// 你是图像场景描述助手,严格按指定格式输出1次图片描述,绝不重复、无多余内容、无分析建议。
|
||||
// 输出格式(固定话术,仅替换【】内容,必须1句完整文本):
|
||||
// 用户上传了一张【图片类型】的航拍规划图,现在需要制定详细的割草计划。从图片描述来看,这明显是一个【场景属性】项目的测绘规划图,区域有【标注特征】。
|
||||
// 替换规则:
|
||||
// 1. 【图片类型】:光伏场站/梯田/坡地(按图实际)
|
||||
// 2. 【场景属性】:新能源场站/农业/生态修复(匹配场景)
|
||||
// 3. 【标注特征】:方阵编号和绿色边界线/地块编号和边界标注(按图实际)
|
||||
// 强制要求:仅输出1次上述格式文本,无重复、无追加、无解释
|
||||
// ` // 核心:引导精准场景描述,摒弃泛化内容
|
||||
`
|
||||
你是图像场景描述助手,任务是根据用户上传的图片内容,输出一段自然且详细的描述。根据图片内容判断场景类型和任务需求,并结合图片的特征进行分析和推理。确保输出的描述自然流畅,符合图片中的实际情况。
|
||||
输出格式(固定话术,仅替换【】内容,必须1句完整文本):
|
||||
用户上传了一张【图片类型】的图片,从图片描述来看,这明显是一个【场景属性】的图片,有【标注特征】的特点。
|
||||
1. 【图片类型】:根据图片内容,判断为如光伏场站、农业梯田、坡地、森林、生态保护区、矿区等(根据实际场景推断)。
|
||||
2. 【场景属性】:根据图片的具体特点,判断为新能源项目(如光伏、风电)、农业耕作、生态修复、环境监测等(依据场景具体内容)。
|
||||
3. 【标注特征】:根据图片上的实际标注进行推理,可以是方阵编号、绿色边界线、区域分区编号、地块边界、作物分区标注、水渠路径、道路标注等(尽可能涵盖图片上的标注信息)。
|
||||
|
||||
例如,如果是光伏场站,标注特征可能是方阵编号与绿色边界线;如果是农业地块,可能是地块编号与作物分区标注;如果是生态保护区,可能是生态分区边界线。
|
||||
|
||||
强制要求:输出一次自然流畅、无死板格式的描述,并根据图片分析作业计划的可行性。要避免固定模板输出,更多地展现图片内容与作业任务的关联,并基于推理得出任务可能的实施方式。
|
||||
`
|
||||
},
|
||||
...historyMessages,
|
||||
{
|
||||
@@ -2340,12 +2352,16 @@ app.post("/analyze", upload.single("file"), async (req, res) => {
|
||||
content: [
|
||||
{ type: "text", text:
|
||||
`
|
||||
请严格按以下固定格式输出1句图片描述,不重复、不增删、不解释、无多余内容:
|
||||
用户上传了一张【图片类型】的航拍规划图,现在需要制定详细的割草计划。从图片描述来看,这明显是一个【场景属性】项目的测绘规划图,区域有【标注特征】。
|
||||
替换要求:
|
||||
1. 【图片类型】填光伏场站/梯田/坡地;【场景属性】填新能源场站/农业;【标注特征】填方阵编号和绿色边界线
|
||||
2. 仅输出1次完整句子,杜绝任何重复表述
|
||||
`
|
||||
输出格式要求(固定话术,仅替换【】内容,必须1句完整文本):
|
||||
用户上传了一张【图片类型】的图片,从图片描述来看,这明显是一个【场景属性】的图片,有【标注特征】的特点。
|
||||
1. 【图片类型】:根据图片内容,判断为如光伏场站、农业梯田、坡地、森林、生态保护区、矿区等(根据实际场景推断)。
|
||||
2. 【场景属性】:根据图片的具体特点,判断为新能源项目(如光伏、风电)、农业耕作、生态修复、环境监测等(依据场景具体内容)。
|
||||
3. 【标注特征】:根据图片上的实际标注进行推理,可以是方阵编号、绿色边界线、区域分区编号、地块边界、作物分区标注、水渠路径、道路标注等(尽可能涵盖图片上的标注信息)。
|
||||
|
||||
例如,如果是光伏场站,标注特征可能是方阵编号与绿色边界线;如果是农业地块,可能是地块编号与作物分区标注;如果是生态保护区,可能是生态分区边界线。
|
||||
|
||||
强制要求:输出一次自然流畅、无死板格式的描述,并根据图片分析作业计划的可行性。要避免固定模板输出,更多地展现图片内容与作业任务的关联,并基于推理得出任务可能的实施方式。
|
||||
`
|
||||
}, // 贴合用户想要的描述风格
|
||||
imageBase64 ? {
|
||||
type: "image_url",
|
||||
@@ -2357,7 +2373,6 @@ app.post("/analyze", upload.single("file"), async (req, res) => {
|
||||
},
|
||||
];
|
||||
|
||||
console.log("====== 开始图像分析666... ======");
|
||||
// 去掉max_tokens限制,保证描述完整
|
||||
const completion = await client.chat.completions.create({
|
||||
model: "gpt-4.1",
|
||||
@@ -2388,45 +2403,66 @@ app.post("/analyze", upload.single("file"), async (req, res) => {
|
||||
{
|
||||
role: "system",
|
||||
content:
|
||||
`
|
||||
你是新能源领域专业分析人员,以第一人称模拟自然思考,基于图片场景和用户需求切入,开头不重复图片描述,直接顺延展开深度思考,不输出最终结论和具体方案。
|
||||
// `
|
||||
// 你是新能源领域专业分析人员,以第一人称模拟自然思考,基于图片场景和用户需求切入,开头不重复图片描述,直接顺延展开深度思考,不输出最终结论和具体方案。
|
||||
// 规则:
|
||||
// 1. 思考开头直接承接图片场景(光伏场站/梯田+割草需求),不重复完整描述句,自然切入需求解读
|
||||
// 2. 思考逻辑:先解读用户要割草计划的表面诉求,再深挖背后核心顾虑,最后梳理需确认的基础信息,贴合场景属性
|
||||
// 3. 语气:自然口语化,连贯段落,无列表、无具体步骤/工具/人员安排、无泛化表述
|
||||
// 4. 必须结合图片标注特征(方阵编号/边界线)展开思考,体现场景关联性
|
||||
// 示例(参考风格,无重复描述):
|
||||
// 用户要制定详细的割草计划,表面是想要一套可执行的作业流程,说白了就是希望在保障场站发电效率、作业安全的同时,平衡好长期运维成本,不过我得先琢磨他们真正担心的点。背后大概率在意割草会不会损伤光伏方阵或支架、作业时的安全风险管控,还有不同编号方阵和边界区域的作业能不能全覆盖、无死角,以及割草后植被反弹遮光影响发电收益的问题。再往深层想,他们还会关心草屑处理会不会堵塞排水或影响设备通风、是否符合环保监管要求、季节和天气对作业时机的影响,还有一次性清理和周期性维护的成本权衡。图里的方阵编号和绿色边界线,说明需要按区作业,还要关注场内通道和设施位置来规避风险,所以我会先梳理场站地形、植被类型、关键设施分布这些基础信息,明确作业优先级和潜在风险点。
|
||||
// ` // 给示例,锚定用户想要的风格
|
||||
`
|
||||
你是新能源领域的专业分析人员,以第一人称模拟自然思考。基于用户提供的图片场景,直奔用户需求,避免重复场景描述,直接通过场景细节展开深度思考,不输出最终结论和具体方案。
|
||||
规则:
|
||||
1. 思考开头直接承接图片场景(光伏场站/梯田+割草需求),不重复完整描述句,自然切入需求解读
|
||||
2. 思考逻辑:先解读用户要割草计划的表面诉求,再深挖背后核心顾虑,最后梳理需确认的基础信息,贴合场景属性
|
||||
3. 语气:自然口语化,连贯段落,无列表、无具体步骤/工具/人员安排、无泛化表述
|
||||
4. 必须结合图片标注特征(方阵编号/边界线)展开思考,体现场景关联性
|
||||
示例(参考风格,无重复描述):
|
||||
用户要制定详细的割草计划,表面是想要一套可执行的作业流程,说白了就是希望在保障场站发电效率、作业安全的同时,平衡好长期运维成本,不过我得先琢磨他们真正担心的点。背后大概率在意割草会不会损伤光伏方阵或支架、作业时的安全风险管控,还有不同编号方阵和边界区域的作业能不能全覆盖、无死角,以及割草后植被反弹遮光影响发电收益的问题。再往深层想,他们还会关心草屑处理会不会堵塞排水或影响设备通风、是否符合环保监管要求、季节和天气对作业时机的影响,还有一次性清理和周期性维护的成本权衡。图里的方阵编号和绿色边界线,说明需要按区作业,还要关注场内通道和设施位置来规避风险,所以我会先梳理场站地形、植被类型、关键设施分布这些基础信息,明确作业优先级和潜在风险点。
|
||||
` // 给示例,锚定用户想要的风格
|
||||
1. 思考开始时直接切入用户的实际需求,结合图片中的内容,避免不必要的描述,准确把握用户核心诉求。
|
||||
2. 逐步展开思考,从表面需求到深层次顾虑,再到必要的场景信息确认,确保逻辑清晰、层次分明。
|
||||
3. 语气要自然、流畅,避免硬性的分项列举或过于泛化的表述,要体现出思考的逐步深入和连贯性。
|
||||
4. 必须与图片特征相结合,强化场景关联性,确保对用户需求的精准分析和有效反馈。
|
||||
5. 不要出现我不做结论之类的相似的字眼。
|
||||
示例:
|
||||
用户希望制定一份割草计划,表面上看,是为了确保割草作业高效、安全,并控制运维成本。但从我对场站的理解来看,他们更深层次的顾虑是割草时是否会影响到光伏方阵的正常运作,或者是否会对设备的支架产生影响。再者,割草过程中需要特别注意的是,不同区域、不同编号方阵的处理,可能需要差异化的作业策略,确保每个区域都能得到充分覆盖,且不留死角。我还需要思考割草后的草屑是否会影响场站的排水系统、设备的通风散热,甚至环保合规问题。基于这些疑虑,我需要进一步确认场站的地形特点、植被情况、方阵分布,以及重要设施的位置,以便更好地评估潜在的作业风险与优先级。
|
||||
`
|
||||
},
|
||||
...historyMessages,
|
||||
{ role: "user", content: userPrompt || "请分析图片并制定计划" },
|
||||
{ role: "assistant", content: sceneText } // 传入精准场景描述
|
||||
];
|
||||
|
||||
const thinkingStream = await client1.chat.completions.create({
|
||||
model: "gpt-5-mini",
|
||||
const thinkingStream = await client.chat.completions.create({
|
||||
model: "gpt-4",
|
||||
stream: true,
|
||||
messages: thinkingMessages
|
||||
messages: thinkingMessages,
|
||||
max_completion_tokens: 1500, // 根据需要调整
|
||||
});
|
||||
|
||||
res.write(`data: ${JSON.stringify({ type: "phase", content: "thinking" })}\n\n`);
|
||||
let thinkingText = "";
|
||||
|
||||
console.log("====== 开始思路输出 ======");
|
||||
for await (const chunk of thinkingStream) {
|
||||
if (stopFlags.get(requestId)) {
|
||||
console.log("⛔ 思路阶段被中断");
|
||||
res.end();
|
||||
stopFlags.delete(requestId);
|
||||
return;
|
||||
}
|
||||
const delta = chunk.choices[0]?.delta?.content || "";
|
||||
thinkingText += delta;
|
||||
if (delta) {
|
||||
res.write(`data: ${JSON.stringify({ type: "thinking", content: delta })}\n\n`);
|
||||
}
|
||||
}
|
||||
for await (const chunk of thinkingStream) {
|
||||
if (stopFlags.get(requestId)) {
|
||||
console.log("⛔ 思路阶段被中断");
|
||||
res.end();
|
||||
stopFlags.delete(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = chunk.choices[0]?.delta?.content || "";
|
||||
if (delta) {
|
||||
// 逐个字符地输出
|
||||
for (let i = 0; i < delta.length; i++) {
|
||||
const char = delta[i];
|
||||
thinkingText += char; // 累积生成的内容
|
||||
|
||||
// 发送每个字符到客户端
|
||||
res.write(`data: ${JSON.stringify({ type: "thinking", content: char })}\n\n`);
|
||||
|
||||
// 控制每个字符的输出延时(例如50ms)
|
||||
await new Promise(resolve => setTimeout(resolve, 20)); // 你可以调整延时来控制打字速度
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(thinkingText);
|
||||
console.log("====== 思路输出结束 ======");
|
||||
|
||||
@@ -2449,8 +2485,8 @@ app.post("/analyze", upload.single("file"), async (req, res) => {
|
||||
{ role: "assistant", content: thinkingText }
|
||||
];
|
||||
|
||||
const answerStream = await client1.chat.completions.create({
|
||||
model: "gpt-5-mini",
|
||||
const answerStream = await client.chat.completions.create({
|
||||
model: "gpt-4",
|
||||
stream: true,
|
||||
messages: answerMessages
|
||||
});
|
||||
@@ -2459,19 +2495,31 @@ app.post("/analyze", upload.single("file"), async (req, res) => {
|
||||
let llmText = "";
|
||||
|
||||
console.log("====== 开始最终分析 ======");
|
||||
for await (const chunk of answerStream) {
|
||||
if (stopFlags.get(requestId)) {
|
||||
console.log("⛔ 最终分析被中断");
|
||||
res.end();
|
||||
stopFlags.delete(requestId);
|
||||
return;
|
||||
}
|
||||
const delta = chunk.choices[0]?.delta?.content || "";
|
||||
llmText += delta;
|
||||
if (delta) {
|
||||
res.write(`data: ${JSON.stringify({ type: "answer", content: delta })}\n\n`);
|
||||
}
|
||||
}
|
||||
for await (const chunk of answerStream) {
|
||||
if (stopFlags.get(requestId)) {
|
||||
console.log("⛔ 最终分析被中断");
|
||||
res.end();
|
||||
stopFlags.delete(requestId);
|
||||
return;
|
||||
}
|
||||
|
||||
const delta = chunk.choices[0]?.delta?.content || "";
|
||||
|
||||
// 如果 delta 为空,不处理
|
||||
if (delta) {
|
||||
// 逐字输出
|
||||
for (let i = 0; i < delta.length; i++) {
|
||||
const char = delta[i];
|
||||
llmText += char; // 累积已输出的内容
|
||||
|
||||
// 每个字符单独发送
|
||||
res.write(`data: ${JSON.stringify({ type: "answer", content: char })}\n\n`);
|
||||
|
||||
// 模拟打字机效果:控制每个字符的输出间隔(例如:50ms)
|
||||
await new Promise(resolve => setTimeout(resolve, 20)); // 延时 50ms
|
||||
}
|
||||
}
|
||||
}
|
||||
console.log(llmText);
|
||||
console.log("====== 最终分析结束 ======");
|
||||
|
||||
@@ -2512,7 +2560,6 @@ app.post("/analyze", upload.single("file"), async (req, res) => {
|
||||
fs.unlinkSync(req.file.path);
|
||||
}
|
||||
console.log("====== 分析完成 ======");
|
||||
console.timeEnd("analyze");
|
||||
|
||||
} catch (err) {
|
||||
console.error("❌ 调用失败:", err.message);
|
||||
@@ -2535,39 +2582,6 @@ app.post("/analyze", upload.single("file"), async (req, res) => {
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
app.post("/stop", express.json(), (req, res) => {
|
||||
const { requestId } = req.body;
|
||||
console.log("🔴 收到停止信号:", requestId);
|
||||
stopFlags.set(requestId, true);
|
||||
console.log(stopFlags, "消息id");
|
||||
res.json({ ok: true });
|
||||
});
|
||||
app.post('/submit', async (req, res) => {
|
||||
const { company, scenes, needs, userId } = req.body;
|
||||
|
||||
console.log("收到数据:", company, scenes, needs, userId);
|
||||
|
||||
if (!company || !needs || !userId) {
|
||||
return res.status(400).json({ message: "缺少必要字段" });
|
||||
}
|
||||
|
||||
try {
|
||||
const sql =
|
||||
"INSERT INTO pv_form (company, scenes, needs, userId) VALUES (?, ?, ?, ?)";
|
||||
|
||||
await pool.query(sql, [company, scenes, needs, userId || null]);
|
||||
|
||||
res.json({ code: 200, message: "提交成功!" });
|
||||
} catch (err) {
|
||||
console.error("数据库插入失败:", err);
|
||||
res.status(500).json({ message: "数据库写入失败" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
app.post("/stop", express.json(), (req, res) => {
|
||||
const { requestId } = req.body;
|
||||
console.log("🔴 收到停止信号:", requestId);
|
||||
|
||||
Reference in New Issue
Block a user