Skip to content

Commit 494b87a

Browse files
committed
feat: 大模型节点新增模型参数配置-深度思考开关(enable_thinking)/温度(0-2)/最大输出字数(max_tokens)/随机种子(seed),AiChatRequest扩展透传参数,AiService新增ChatRequestAsync统一请求入口(图片+参数),前端节点面板模型参数区+帮助弹窗更新
1 parent 52baa70 commit 494b87a

5 files changed

Lines changed: 118 additions & 6 deletions

File tree

‎Juggle.Application/Services/Flow/FlowExecutionService.cs‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,9 +68,7 @@ private async Task<string> BuildSkillsPromptAsync(List<long> ids)
6868
private async Task<FlowEngine> BuildEngineAsync(Dictionary<string, DataSourceInfo> dsInfos,
6969
Dictionary<string, string?> staticVars, Func<string, Task<string?>> flowContentLoader)
7070
=> new FlowEngine(_httpClientFactory, dsInfos, staticVars, flowContentLoader,
71-
aiChatFunc: req => req.Images.Count > 0
72-
? _aiService.ChatWithImagesAsync(req.SystemPrompt, req.UserInput, req.Images, req.ProviderId, req.Model)
73-
: _aiService.ChatAsync(req.SystemPrompt, req.UserInput, req.ProviderId, modelOverride: req.Model),
71+
aiChatFunc: req => _aiService.ChatRequestAsync(req),
7472
redisConnStrs: await GetRedisConnStrsAsync(),
7573
kbSearchFunc: (kbId, query, topK) => _kbService.SearchAsContextAsync(kbId, query, topK),
7674
skillResolver: BuildSkillsPromptAsync);

‎Juggle.Application/Services/Impl/AiService.cs‎

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System.Text;
22
using System.Text.Json;
3+
using Juggle.Domain.Engine.NodeExecutors;
34
using Juggle.Domain.Entities;
45
using Juggle.Infrastructure.Persistence;
56
using Microsoft.EntityFrameworkCore;
@@ -185,6 +186,58 @@ public async Task<string> ChatMessagesAsync(List<(string Role, string Content)>
185186
return content?.Trim() ?? "";
186187
}
187188

189+
/// <summary>按 AI 节点请求调用大模型(统一入口:支持图片输入、温度/最大输出字数/随机种子/深度思考等模型参数)。</summary>
190+
public async Task<string> ChatRequestAsync(AiChatRequest req)
191+
{
192+
var cfg = await ResolveConfigAsync(req.ProviderId > 0 ? req.ProviderId : null);
193+
if (!string.IsNullOrEmpty(req.Model)) cfg.Model = req.Model;
194+
if (string.IsNullOrEmpty(cfg.BaseUrl) || string.IsNullOrEmpty(cfg.ApiKey))
195+
throw new Exception("未配置 AI 大模型:请在系统设置 → 大模型设置中添加并启用供应商");
196+
197+
var url = cfg.BaseUrl.TrimEnd('/');
198+
if (!url.EndsWith("/chat/completions", StringComparison.OrdinalIgnoreCase))
199+
url += "/chat/completions";
200+
201+
// 图片输入 → 多模态 content 数组
202+
object userContent = req.UserInput;
203+
if (req.Images is { Count: > 0 })
204+
{
205+
var content = new List<object>();
206+
if (!string.IsNullOrWhiteSpace(req.UserInput))
207+
content.Add(new { type = "text", text = req.UserInput });
208+
foreach (var img in req.Images)
209+
content.Add(new { type = "image_url", image_url = new { url = img } });
210+
userContent = content.ToArray();
211+
}
212+
213+
var payload = new Dictionary<string, object?>
214+
{
215+
["model"] = cfg.Model,
216+
["messages"] = new object[]
217+
{
218+
new { role = "system", content = req.SystemPrompt },
219+
new { role = "user", content = userContent }
220+
},
221+
["temperature"] = req.Temperature ?? 0.2
222+
};
223+
if (req.MaxTokens is > 0) payload["max_tokens"] = req.MaxTokens.Value;
224+
if (req.Seed.HasValue) payload["seed"] = req.Seed.Value;
225+
if (req.EnableThinking) payload["enable_thinking"] = true;
226+
227+
var client = _httpClientFactory.CreateClient();
228+
client.Timeout = TimeSpan.FromSeconds(180);
229+
using var reqMsg = new HttpRequestMessage(HttpMethod.Post, url);
230+
reqMsg.Headers.TryAddWithoutValidation("Authorization", $"Bearer {cfg.ApiKey}");
231+
reqMsg.Content = new StringContent(JsonSerializer.Serialize(payload), Encoding.UTF8, "application/json");
232+
233+
using var resp = await client.SendAsync(reqMsg);
234+
var body = await resp.Content.ReadAsStringAsync();
235+
if (!resp.IsSuccessStatusCode)
236+
throw new Exception($"AI 调用失败({(int)resp.StatusCode}): {Truncate(body, 300)}");
237+
using var doc = JsonDocument.Parse(body);
238+
return doc.RootElement.GetProperty("choices")[0].GetProperty("message").GetProperty("content").GetString()?.Trim() ?? "";
239+
}
240+
188241
private static string Truncate(string s, int len) => s.Length <= len ? s : s[..len] + "...";
189242

190243
// ==================== 流程生成 ====================

‎Juggle.Domain/Engine/FlowModels.cs‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -304,6 +304,22 @@ public class AiConfig
304304
/// <summary>技能 ID 列表(逗号分隔,执行时技能内容拼入系统提示词)</summary>
305305
[JsonPropertyName("skills")]
306306
public string Skills { get; set; } = "";
307+
308+
/// <summary>是否启用深度思考(发送 enable_thinking 参数,Qwen/DeepSeek 等推理模型生效)</summary>
309+
[JsonPropertyName("enableThinking")]
310+
public bool EnableThinking { get; set; }
311+
312+
/// <summary>温度(0-2,越大越发散;空=服务默认)</summary>
313+
[JsonPropertyName("temperature")]
314+
public double? Temperature { get; set; }
315+
316+
/// <summary>最大输出字数(max_tokens,0/空=不限制)</summary>
317+
[JsonPropertyName("maxTokens")]
318+
public int? MaxTokens { get; set; }
319+
320+
/// <summary>随机种子(seed,固定后输出更稳定;空=随机)</summary>
321+
[JsonPropertyName("seed")]
322+
public int? Seed { get; set; }
307323
}
308324

309325
/// <summary>文件解析节点配置(FILE_PARSE 节点)</summary>

‎Juggle.Domain/Engine/NodeExecutors/AiNodeExecutor.cs‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
namespace Juggle.Domain.Engine.NodeExecutors;
22

33
/// <summary>大模型调用请求(AI 节点与文件解析图片识别共用)。</summary>
4-
public record AiChatRequest(string SystemPrompt, string UserInput, List<string> Images, long ProviderId, string? Model);
4+
public record AiChatRequest(string SystemPrompt, string UserInput, List<string> Images, long ProviderId, string? Model,
5+
double? Temperature = null, int? MaxTokens = null, int? Seed = null, bool EnableThinking = false);
56

67
/// <summary>
78
/// AI 大模型节点执行器:把输入变量内容作为用户消息、按系统提示词调用大模型,
@@ -56,7 +57,8 @@ public AiNodeExecutor(Func<AiChatRequest, Task<string>> chat, Func<List<long>, T
5657
}
5758
}
5859

59-
var reply = await _chat(new AiChatRequest(systemPrompt, input, images, cfg.ProviderId, string.IsNullOrWhiteSpace(cfg.Model) ? null : cfg.Model));
60+
var reply = await _chat(new AiChatRequest(systemPrompt, input, images, cfg.ProviderId, string.IsNullOrWhiteSpace(cfg.Model) ? null : cfg.Model,
61+
cfg.Temperature, cfg.MaxTokens, cfg.Seed, cfg.EnableThinking));
6062

6163
if (!string.IsNullOrWhiteSpace(cfg.Output))
6264
{

‎JuggleNet6.Frontend/src/views/flow/FlowDesign.vue‎

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,6 +328,29 @@
328328
</el-select>
329329
<div style="font-size:11px;color:#909399;margin-top:4px">技能在 系统设置 → Skill 管理 中维护;执行时按「技能名 + 内容」拼入系统提示词。</div>
330330
</div>
331+
<div class="prop-item">
332+
<label>模型参数</label>
333+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
334+
<span style="width:64px;font-size:12px;color:#666;flex-shrink:0">深度思考</span>
335+
<el-switch v-model="selectedNode.aiConfig.enableThinking" size="small" />
336+
<span style="font-size:11px;color:#909399">推理模型(deepseek-reasoner / Qwen 思考模式)生效</span>
337+
</div>
338+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
339+
<span style="width:64px;font-size:12px;color:#666;flex-shrink:0">温度</span>
340+
<el-slider v-model="aiTempModel" size="small" style="flex:1" :min="0" :max="2" :step="0.1" :format-tooltip="aiTempFormat" />
341+
<span style="width:36px;font-size:12px;color:#666;text-align:right;flex-shrink:0">{{ aiTempModel.toFixed(1) }}</span>
342+
</div>
343+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
344+
<span style="width:64px;font-size:12px;color:#666;flex-shrink:0">最大输出</span>
345+
<el-input-number v-model="aiMaxTokensModel" size="small" style="flex:1" :min="0" :step="100" controls-position="right" />
346+
<span style="font-size:11px;color:#909399;flex-shrink:0">0=不限制</span>
347+
</div>
348+
<div style="display:flex;align-items:center;gap:8px">
349+
<span style="width:64px;font-size:12px;color:#666;flex-shrink:0">随机种子</span>
350+
<el-input-number v-model="aiSeedModel" size="small" style="flex:1" :min="0" :step="1" controls-position="right" />
351+
<span style="font-size:11px;color:#909399;flex-shrink:0">0=随机</span>
352+
</div>
353+
</div>
331354
<div class="prop-item">
332355
<label>输出目标</label>
333356
<div style="display:flex;gap:4px">
@@ -1980,6 +2003,21 @@ const skillIdsModel = computed({
19802003
}
19812004
})
19822005
2006+
// AI 节点模型参数:温度(默认 0.7)/ 最大输出字数(0=不限)/ 随机种子(0=随机)
2007+
const aiTempModel = computed<number>({
2008+
get: () => selectedNode.value?.aiConfig?.temperature ?? 0.7,
2009+
set: (v: number) => { if (selectedNode.value?.aiConfig) selectedNode.value.aiConfig.temperature = v }
2010+
})
2011+
const aiMaxTokensModel = computed<number>({
2012+
get: () => selectedNode.value?.aiConfig?.maxTokens || 0,
2013+
set: (v: number) => { if (selectedNode.value?.aiConfig) selectedNode.value.aiConfig.maxTokens = v > 0 ? v : null }
2014+
})
2015+
const aiSeedModel = computed<number>({
2016+
get: () => selectedNode.value?.aiConfig?.seed || 0,
2017+
set: (v: number) => { if (selectedNode.value?.aiConfig) selectedNode.value.aiConfig.seed = v > 0 ? v : null }
2018+
})
2019+
function aiTempFormat(v: number) { return v.toFixed(1) }
2020+
19832021
// 按所选供应商的"支持的能力"过滤技能列表(供应商配置了 skills 时只显示其支持的)
19842022
const aiNodeSkillOptions = computed(() => {
19852023
const provider = aiProviders.value.find((p: any) => p.id === (selectedNode.value?.aiConfig?.providerId || 0))
@@ -2211,6 +2249,11 @@ env_delay_ms = env_retry_count * 1000 + 500` },
22112249
输入变量: 作为用户消息发送给模型(可选流程入参/中间变量/出参)
22122250
图片输入: 多选图片变量(data URL),配置后走视觉模型识别(多模态)
22132251
系统提示词: 人设与任务说明(如"你是文案专家,输出简洁有力的文案")
2252+
模型参数:
2253+
深度思考: 开启后发送 enable_thinking 参数(推理模型如 deepseek-reasoner / Qwen 思考模式生效)
2254+
温度: 0-2 随机性(越大越发散),默认 0.7
2255+
最大输出: 最大输出字数(0=不限制)
2256+
随机种子: 固定后同输入输出更稳定(0=随机)
22142257
输出目标: 变量 / 出参 / 入参 + 对应参数选择(模型回复写入)` },
22152258
{ title: '三、使用 demo', code: `// 场景:流程中生成商品推荐文案
22162259
前置 ASSIGN 节点: env_product_name = "智能手表"
@@ -3327,7 +3370,7 @@ function addNode(type: string) {
33273370
targetType: 'VARIABLE', targetCode: '', template: ''
33283371
}
33293372
if (type === 'AI') bNode.aiConfig = {
3330-
input: '', systemPrompt: '', output: ''
3373+
input: '', systemPrompt: '', output: '', enableThinking: false, temperature: 0.7
33313374
}
33323375
if (type === 'FILE_PARSE') bNode.fileParseConfig = {
33333376
input: '', fileType: 'auto', output: ''

0 commit comments

Comments
 (0)