aaa
Some checks failed
Flask 提示词大师 - CI/CD 流水线 / 代码质量检查 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 单元测试 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 集成测试 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 构建Docker镜像 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署到测试环境 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署到生产环境 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署监控系统 (push) Has been cancelled
Some checks failed
Flask 提示词大师 - CI/CD 流水线 / 代码质量检查 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 单元测试 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 集成测试 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 构建Docker镜像 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署到测试环境 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署到生产环境 (push) Has been cancelled
Flask 提示词大师 - CI/CD 流水线 / 部署监控系统 (push) Has been cancelled
This commit is contained in:
@@ -1,17 +1,29 @@
|
||||
# 提示词智能优化 Android App
|
||||
# 提示词智能优化(本地模型)Android App
|
||||
|
||||
调用提示词大师 3 号专家接口,实现智能提示词生成。
|
||||
基于《生成专业提示词代码逻辑分析》,本地调用 LLM 实现智能提示词生成,**不依赖远程 Flask API**。
|
||||
|
||||
## 功能
|
||||
|
||||
- **两阶段专家逻辑**:先做意图分析(技术/创意/分析/咨询),再按领域选择专家模板生成提示词
|
||||
- **本地模型调用**:通过 OpenAI 兼容接口(如 Ollama)本地推理
|
||||
- **配置灵活**:支持设置 API 地址和模型名称
|
||||
|
||||
## 环境
|
||||
|
||||
- Android Studio(推荐使用 AGP 8.7+、Gradle 8.9)
|
||||
- Android Studio(推荐 AGP 8.7+、Gradle 8.9)
|
||||
- minSdk 24,targetSdk 34
|
||||
- Java 17
|
||||
|
||||
## 接口
|
||||
## 使用前准备
|
||||
|
||||
- 地址:`POST http://101.43.95.130:5002/api/open/expert-generate-3`
|
||||
- 参数:`input_text`(必填)、`temperature`、`max_tokens`、`timeout`、`uid`
|
||||
1. **安装 Ollama(或其他 OpenAI 兼容服务)**
|
||||
- 官网:https://ollama.ai
|
||||
- 运行:`ollama run qwen2:0.5b`(或其它兼容模型)
|
||||
|
||||
2. **配置 API 地址**
|
||||
- 模拟器:`http://10.0.2.2:11434/v1/`(10.0.2.2 指向宿主机 localhost)
|
||||
- 真机:`http://<电脑局域网 IP>:11434/v1/`(如 `http://192.168.1.100:11434/v1/`)
|
||||
- 在 App 内点击「设置」可修改地址和模型名
|
||||
|
||||
## 构建
|
||||
|
||||
@@ -24,6 +36,6 @@ cd exampleAiApp01
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 需配置网络权限,并允许明文 HTTP(101.43.95.130 为 HTTP)
|
||||
- 确保设备可访问 `101.43.95.130:5002`
|
||||
- 需配置网络权限,并允许明文 HTTP(`network_security_config.xml` 已配置)
|
||||
- 确保设备与 Ollama 所在电脑处于同一局域网(真机)或使用模拟器
|
||||
- 如缺少启动图标,可在 Android Studio 中通过 `File > New > Image Asset` 生成
|
||||
|
||||
@@ -10,6 +10,10 @@
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.PromptOptimizer"
|
||||
android:usesCleartextTraffic="true">
|
||||
<activity
|
||||
android:name=".SettingsActivity"
|
||||
android:exported="false"
|
||||
android:theme="@style/Theme.PromptOptimizer" />
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.example.promptoptimizer;
|
||||
import android.content.ClipData;
|
||||
import android.content.ClipboardManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
@@ -13,20 +14,19 @@ import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.example.promptoptimizer.api.ExpertGenerate3Api;
|
||||
import com.example.promptoptimizer.model.ExpertGenerate3Request;
|
||||
import com.example.promptoptimizer.llm.LocalLlmClient;
|
||||
import com.example.promptoptimizer.llm.PromptGenerator;
|
||||
import com.example.promptoptimizer.model.ExpertGenerate3Response;
|
||||
import com.google.android.material.button.MaterialButton;
|
||||
import com.google.android.material.card.MaterialCardView;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
/**
|
||||
* 提示词智能优化 - 主界面
|
||||
* 提示词智能优化 - 本地模型调用(两阶段专家逻辑)
|
||||
* 参考《生成专业提示词代码逻辑分析》
|
||||
*/
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
@@ -34,6 +34,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
private EditText etInput;
|
||||
private MaterialButton btnGenerate;
|
||||
private MaterialButton btnSettings;
|
||||
private ProgressBar progressBar;
|
||||
private TextView tvLoading;
|
||||
private MaterialCardView cardResult;
|
||||
@@ -42,14 +43,21 @@ public class MainActivity extends AppCompatActivity {
|
||||
private MaterialButton btnCopy;
|
||||
|
||||
private boolean isSubmitting = false;
|
||||
private LocalLlmClient llmClient;
|
||||
private PromptGenerator promptGenerator;
|
||||
private final ExecutorService executor = Executors.newSingleThreadExecutor();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
llmClient = new LocalLlmClient(this);
|
||||
promptGenerator = new PromptGenerator(llmClient);
|
||||
|
||||
etInput = findViewById(R.id.etInput);
|
||||
btnGenerate = findViewById(R.id.btnGenerate);
|
||||
btnSettings = findViewById(R.id.btnSettings);
|
||||
progressBar = findViewById(R.id.progressBar);
|
||||
tvLoading = findViewById(R.id.tvLoading);
|
||||
cardResult = findViewById(R.id.cardResult);
|
||||
@@ -59,6 +67,15 @@ public class MainActivity extends AppCompatActivity {
|
||||
|
||||
btnGenerate.setOnClickListener(v -> doGenerate());
|
||||
btnCopy.setOnClickListener(v -> copyPrompt());
|
||||
if (btnSettings != null) {
|
||||
btnSettings.setOnClickListener(v -> startActivity(new Intent(this, SettingsActivity.class)));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
llmClient.resetApi();
|
||||
}
|
||||
|
||||
private void doGenerate() {
|
||||
@@ -76,45 +93,27 @@ public class MainActivity extends AppCompatActivity {
|
||||
btnGenerate.setEnabled(false);
|
||||
cardResult.setVisibility(View.GONE);
|
||||
|
||||
ExpertGenerate3Request request = new ExpertGenerate3Request(input);
|
||||
ExpertGenerate3Api api = RetrofitClient.getApi();
|
||||
|
||||
api.generate(request).enqueue(new Callback<ExpertGenerate3Response>() {
|
||||
@Override
|
||||
public void onResponse(Call<ExpertGenerate3Response> call,
|
||||
Response<ExpertGenerate3Response> response) {
|
||||
isSubmitting = false;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
tvLoading.setVisibility(View.GONE);
|
||||
btnGenerate.setEnabled(true);
|
||||
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
ExpertGenerate3Response body = response.body();
|
||||
if (body.getCode() == 200 && body.getData() != null) {
|
||||
showResult(body);
|
||||
} else {
|
||||
String msg = body.getMessage() != null ? body.getMessage() : getString(R.string.error_network);
|
||||
Log.w(TAG, "API 业务错误: code=" + body.getCode() + ", message=" + msg);
|
||||
Toast.makeText(MainActivity.this, msg, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} else {
|
||||
String err = "HTTP " + response.code() + (response.errorBody() != null ? ": " + response.message() : "");
|
||||
Log.e(TAG, "API 请求失败: " + err);
|
||||
Toast.makeText(MainActivity.this, getString(R.string.error_network) + " " + err,
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
ExpertGenerate3Response result = promptGenerator.generate(input, 0.7f, 1000);
|
||||
runOnUiThread(() -> {
|
||||
isSubmitting = false;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
tvLoading.setVisibility(View.GONE);
|
||||
btnGenerate.setEnabled(true);
|
||||
showResult(result);
|
||||
});
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, "本地生成失败", e);
|
||||
runOnUiThread(() -> {
|
||||
isSubmitting = false;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
tvLoading.setVisibility(View.GONE);
|
||||
btnGenerate.setEnabled(true);
|
||||
Toast.makeText(MainActivity.this,
|
||||
getString(R.string.error_network) + ": " + e.getMessage(),
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ExpertGenerate3Response> call, Throwable t) {
|
||||
isSubmitting = false;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
tvLoading.setVisibility(View.GONE);
|
||||
btnGenerate.setEnabled(true);
|
||||
String detail = t != null ? t.getMessage() : "unknown";
|
||||
Log.e(TAG, "网络请求失败: " + detail, t);
|
||||
Toast.makeText(MainActivity.this, getString(R.string.error_network) + ": " + detail,
|
||||
Toast.LENGTH_LONG).show();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -151,4 +150,10 @@ public class MainActivity extends AppCompatActivity {
|
||||
Toast.makeText(this, R.string.toast_copied, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
executor.shutdown();
|
||||
super.onDestroy();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.example.promptoptimizer;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import com.example.promptoptimizer.llm.LlmConfig;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
||||
/**
|
||||
* 本地 LLM 配置
|
||||
*/
|
||||
public class SettingsActivity extends AppCompatActivity {
|
||||
|
||||
private TextInputEditText etBaseUrl;
|
||||
private TextInputEditText etModel;
|
||||
private Button btnSave;
|
||||
private LlmConfig config;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_settings);
|
||||
|
||||
config = new LlmConfig(this);
|
||||
etBaseUrl = findViewById(R.id.etBaseUrl);
|
||||
etModel = findViewById(R.id.etModel);
|
||||
btnSave = findViewById(R.id.btnSave);
|
||||
|
||||
etBaseUrl.setText(config.getBaseUrl());
|
||||
etModel.setText(config.getModel());
|
||||
|
||||
btnSave.setOnClickListener(v -> {
|
||||
String url = etBaseUrl.getText() != null ? etBaseUrl.getText().toString().trim() : "";
|
||||
String model = etModel.getText() != null ? etModel.getText().toString().trim() : "";
|
||||
if (url.isEmpty()) {
|
||||
Toast.makeText(this, "请填写 API 地址", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
config.setBaseUrl(url);
|
||||
config.setModel(model.isEmpty() ? "qwen2:0.5b" : model);
|
||||
Toast.makeText(this, "已保存", Toast.LENGTH_SHORT).show();
|
||||
finish();
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.example.promptoptimizer.llm;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OpenAI 兼容的 Chat Completions 请求
|
||||
*/
|
||||
public class ChatCompletionRequest {
|
||||
|
||||
@SerializedName("model")
|
||||
private String model;
|
||||
|
||||
@SerializedName("messages")
|
||||
private List<Message> messages;
|
||||
|
||||
@SerializedName("temperature")
|
||||
private Float temperature;
|
||||
|
||||
@SerializedName("max_tokens")
|
||||
private Integer maxTokens;
|
||||
|
||||
@SerializedName("stream")
|
||||
private Boolean stream = false;
|
||||
|
||||
public ChatCompletionRequest(String model, List<Message> messages, Float temperature, Integer maxTokens) {
|
||||
this.model = model;
|
||||
this.messages = messages;
|
||||
this.temperature = temperature;
|
||||
this.maxTokens = maxTokens;
|
||||
}
|
||||
|
||||
public static class Message {
|
||||
@SerializedName("role")
|
||||
private String role;
|
||||
|
||||
@SerializedName("content")
|
||||
private String content;
|
||||
|
||||
public Message(String role, String content) {
|
||||
this.role = role;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
public String getRole() { return role; }
|
||||
public String getContent() { return content; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.example.promptoptimizer.llm;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* OpenAI 兼容的 Chat Completions 响应
|
||||
*/
|
||||
public class ChatCompletionResponse {
|
||||
|
||||
@SerializedName("choices")
|
||||
private List<Choice> choices;
|
||||
|
||||
public String getContent() {
|
||||
if (choices != null && !choices.isEmpty()) {
|
||||
Choice c = choices.get(0);
|
||||
if (c != null && c.getMessage() != null) {
|
||||
return c.getMessage().getContent();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static class Choice {
|
||||
@SerializedName("message")
|
||||
private Message message;
|
||||
|
||||
public Message getMessage() { return message; }
|
||||
}
|
||||
|
||||
public static class Message {
|
||||
@SerializedName("content")
|
||||
private String content;
|
||||
|
||||
public String getContent() { return content; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.example.promptoptimizer.llm;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
/**
|
||||
* 本地 LLM 配置
|
||||
*/
|
||||
public class LlmConfig {
|
||||
|
||||
private static final String PREFS = "llm_config";
|
||||
private static final String KEY_BASE_URL = "base_url";
|
||||
private static final String KEY_MODEL = "model";
|
||||
|
||||
// 默认:Ollama 本地。模拟器用 10.0.2.2 访问宿主机
|
||||
private static final String DEFAULT_BASE_URL = "http://10.0.2.2:11434/v1/";
|
||||
private static final String DEFAULT_MODEL = "qwen2:0.5b";
|
||||
|
||||
private final SharedPreferences prefs;
|
||||
|
||||
public LlmConfig(Context context) {
|
||||
prefs = context.getApplicationContext().getSharedPreferences(PREFS, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
public String getBaseUrl() {
|
||||
return prefs.getString(KEY_BASE_URL, DEFAULT_BASE_URL);
|
||||
}
|
||||
|
||||
public void setBaseUrl(String url) {
|
||||
if (url != null && !url.endsWith("/")) url = url + "/";
|
||||
prefs.edit().putString(KEY_BASE_URL, url).apply();
|
||||
}
|
||||
|
||||
public String getModel() {
|
||||
return prefs.getString(KEY_MODEL, DEFAULT_MODEL);
|
||||
}
|
||||
|
||||
public void setModel(String model) {
|
||||
prefs.edit().putString(KEY_MODEL, model != null ? model : DEFAULT_MODEL).apply();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.example.promptoptimizer.llm;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.POST;
|
||||
|
||||
/**
|
||||
* OpenAI 兼容的 LLM API(Ollama / 本地部署等)
|
||||
*/
|
||||
public interface LocalLlmApi {
|
||||
|
||||
@POST("chat/completions")
|
||||
Call<ChatCompletionResponse> chat(@Body ChatCompletionRequest request);
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.example.promptoptimizer.llm;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.logging.HttpLoggingInterceptor;
|
||||
import retrofit2.Retrofit;
|
||||
import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
/**
|
||||
* 本地 LLM 客户端(支持 Ollama 等 OpenAI 兼容 API)
|
||||
*/
|
||||
public class LocalLlmClient {
|
||||
|
||||
private final LlmConfig config;
|
||||
private LocalLlmApi api;
|
||||
|
||||
public LocalLlmClient(Context context) {
|
||||
config = new LlmConfig(context);
|
||||
}
|
||||
|
||||
public LocalLlmApi getApi() {
|
||||
if (api == null) {
|
||||
HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor();
|
||||
interceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
|
||||
|
||||
OkHttpClient client = new OkHttpClient.Builder()
|
||||
.connectTimeout(120, TimeUnit.SECONDS)
|
||||
.readTimeout(120, TimeUnit.SECONDS)
|
||||
.writeTimeout(120, TimeUnit.SECONDS)
|
||||
.addInterceptor(interceptor)
|
||||
.build();
|
||||
|
||||
Retrofit retrofit = new Retrofit.Builder()
|
||||
.baseUrl(config.getBaseUrl())
|
||||
.client(client)
|
||||
.addConverterFactory(GsonConverterFactory.create())
|
||||
.build();
|
||||
api = retrofit.create(LocalLlmApi.class);
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
public void resetApi() {
|
||||
api = null;
|
||||
}
|
||||
|
||||
public String getBaseUrl() { return config.getBaseUrl(); }
|
||||
public String getModel() { return config.getModel(); }
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package com.example.promptoptimizer.llm;
|
||||
|
||||
import android.util.Log;
|
||||
|
||||
import com.example.promptoptimizer.model.ExpertGenerate3Response;
|
||||
import com.google.gson.Gson;
|
||||
import com.google.gson.JsonSyntaxException;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import retrofit2.Response;
|
||||
|
||||
/**
|
||||
* 两阶段专家提示词生成(参考《生成专业提示词代码逻辑分析》)
|
||||
* 第一阶段:意图分析 → 第二阶段:领域专家生成
|
||||
*/
|
||||
public class PromptGenerator {
|
||||
|
||||
private static final String TAG = "PromptGenerator";
|
||||
|
||||
private static final String INTENT_PROMPT = "你是一位资深的意图分析专家,请分析用户输入的意图和需求。\n\n" +
|
||||
"你必须严格按照以下JSON格式返回,不要添加任何其他内容:\n" +
|
||||
"{\n" +
|
||||
" \"core_intent\": \"技术\",\n" +
|
||||
" \"domain\": \"具体专业领域\",\n" +
|
||||
" \"key_requirements\": [\"需求1\", \"需求2\"],\n" +
|
||||
" \"expected_output\": \"期望输出的具体形式\",\n" +
|
||||
" \"constraints\": [\"约束1\", \"约束2\"],\n" +
|
||||
" \"keywords\": [\"关键词1\", \"关键词2\"]\n" +
|
||||
"}\n\n" +
|
||||
"注意:\n" +
|
||||
"1. 严格遵守JSON格式\n" +
|
||||
"2. core_intent必须是以下之一:技术、创意、分析、咨询\n" +
|
||||
"3. 数组至少包含1个元素\n" +
|
||||
"4. 所有字段都必须存在\n" +
|
||||
"5. 不要包含注释\n" +
|
||||
"6. 不要添加任何额外的文本";
|
||||
|
||||
private static final Map<String, String> EXPERT_TEMPLATES = new HashMap<>();
|
||||
static {
|
||||
EXPERT_TEMPLATES.put("技术", "你是一位专业的技术领域提示工程师。基于以下意图分析,生成一个专业的技术任务提示词:\n\n意图分析:\n{analysis}\n\n请生成的提示词包含:\n1. 明确的技术背景和上下文\n2. 具体的技术要求和规范\n3. 性能和质量标准\n4. 技术约束条件\n5. 预期交付成果\n6. 评估标准\n\n使用专业技术术语,确保提示词的可执行性和可验证性。");
|
||||
EXPERT_TEMPLATES.put("创意", "你是一位专业的创意领域提示工程师。基于以下意图分析,生成一个创意设计提示词:\n\n意图分析:\n{analysis}\n\n请生成的提示词包含:\n1. 创意方向和灵感来源\n2. 风格和氛围要求\n3. 目标受众定义\n4. 设计元素规范\n5. 创意表现形式\n6. 评估标准\n\n使用专业创意术语,确保提示词的创新性和可执行性。");
|
||||
EXPERT_TEMPLATES.put("分析", "你是一位专业的数据分析提示工程师。基于以下意图分析,生成一个数据分析提示词:\n\n意图分析:\n{analysis}\n\n请生成的提示词包含:\n1. 分析目标和范围\n2. 数据要求和规范\n3. 分析方法和工具\n4. 输出格式要求\n5. 关键指标定义\n6. 质量控制标准\n\n使用专业分析术语,确保提示词的科学性和可操作性。");
|
||||
EXPERT_TEMPLATES.put("咨询", "你是一位专业的咨询领域提示工程师。基于以下意图分析,生成一个咨询服务提示词:\n\n意图分析:\n{analysis}\n\n请生成的提示词包含:\n1. 咨询问题界定\n2. 背景信息要求\n3. 分析框架设定\n4. 建议输出格式\n5. 实施考虑因素\n6. 效果评估标准\n\n使用专业咨询术语,确保提示词的专业性和实用性。");
|
||||
}
|
||||
|
||||
private static final String FALLBACK_TEMPLATE = "你是一位专业的通用领域提示工程师。基于以下意图分析,生成一个专业的提示词:\n\n意图分析:\n{analysis}\n\n请生成的提示词包含:\n1. 明确的目标定义\n2. 具体要求和规范\n3. 质量标准\n4. 约束条件\n5. 预期输出\n6. 评估标准\n\n确保提示词的清晰性和可执行性。";
|
||||
|
||||
private final LocalLlmClient llmClient;
|
||||
private final Gson gson = new Gson();
|
||||
|
||||
public PromptGenerator(LocalLlmClient llmClient) {
|
||||
this.llmClient = llmClient;
|
||||
}
|
||||
|
||||
/**
|
||||
* 同步执行两阶段生成(需在后台线程调用)
|
||||
*/
|
||||
public ExpertGenerate3Response generate(String userInput, float temperature, int maxTokens) throws Exception {
|
||||
String model = llmClient.getModel();
|
||||
LocalLlmApi api = llmClient.getApi();
|
||||
|
||||
// 第一阶段:意图分析
|
||||
List<ChatCompletionRequest.Message> intentMessages = Arrays.asList(
|
||||
new ChatCompletionRequest.Message("system", INTENT_PROMPT),
|
||||
new ChatCompletionRequest.Message("user", userInput)
|
||||
);
|
||||
ChatCompletionRequest intentReq = new ChatCompletionRequest(model, intentMessages, 0.1f, 1024);
|
||||
Response<ChatCompletionResponse> intentResp = api.chat(intentReq).execute();
|
||||
if (!intentResp.isSuccessful() || intentResp.body() == null) {
|
||||
throw new Exception("意图分析请求失败: " + (intentResp.errorBody() != null ? intentResp.errorBody().string() : intentResp.code()));
|
||||
}
|
||||
String intentRaw = intentResp.body().getContent();
|
||||
if (intentRaw == null || intentRaw.isEmpty()) {
|
||||
throw new Exception("意图分析返回为空");
|
||||
}
|
||||
intentRaw = intentRaw.replace("```json", "").replace("```", "").trim();
|
||||
|
||||
IntentAnalysis intent = parseIntent(intentRaw);
|
||||
if (intent == null) {
|
||||
throw new Exception("意图分析解析失败");
|
||||
}
|
||||
|
||||
// 第二阶段:领域专家生成
|
||||
String template = EXPERT_TEMPLATES.getOrDefault(intent.coreIntent, FALLBACK_TEMPLATE);
|
||||
String analysisStr = gson.toJson(intent.toMap());
|
||||
String expertPrompt = template.replace("{analysis}", analysisStr);
|
||||
|
||||
List<ChatCompletionRequest.Message> expertMessages = Arrays.asList(
|
||||
new ChatCompletionRequest.Message("system", expertPrompt),
|
||||
new ChatCompletionRequest.Message("user", userInput)
|
||||
);
|
||||
ChatCompletionRequest expertReq = new ChatCompletionRequest(model, expertMessages, temperature, maxTokens);
|
||||
Response<ChatCompletionResponse> expertResp = api.chat(expertReq).execute();
|
||||
if (!expertResp.isSuccessful() || expertResp.body() == null) {
|
||||
throw new Exception("提示词生成请求失败: " + expertResp.code());
|
||||
}
|
||||
String generated = expertResp.body().getContent();
|
||||
if (generated == null || generated.isEmpty()) {
|
||||
throw new Exception("生成结果为空");
|
||||
}
|
||||
|
||||
ExpertGenerate3Response result = new ExpertGenerate3Response();
|
||||
result.setCode(200);
|
||||
result.setMessage("success");
|
||||
ExpertGenerate3Response.ResponseData data = new ExpertGenerate3Response.ResponseData();
|
||||
data.setIntentAnalysis(intent.toIntentAnalysis());
|
||||
data.setGeneratedPrompt(generated.trim());
|
||||
result.setData(data);
|
||||
return result;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private IntentAnalysis parseIntent(String json) {
|
||||
try {
|
||||
Map<String, Object> map = gson.fromJson(json, Map.class);
|
||||
if (map == null) return null;
|
||||
|
||||
IntentAnalysis ia = new IntentAnalysis();
|
||||
ia.coreIntent = safeStr(map.get("core_intent"), "技术");
|
||||
if (!Arrays.asList("技术", "创意", "分析", "咨询").contains(ia.coreIntent)) {
|
||||
ia.coreIntent = "技术";
|
||||
}
|
||||
ia.domain = safeStr(map.get("domain"), "未指定");
|
||||
ia.expectedOutput = safeStr(map.get("expected_output"), "未指定");
|
||||
ia.keyRequirements = toStrList(map.get("key_requirements"));
|
||||
ia.constraints = toStrList(map.get("constraints"));
|
||||
ia.keywords = toStrList(map.get("keywords"));
|
||||
return ia;
|
||||
} catch (JsonSyntaxException e) {
|
||||
Log.e(TAG, "JSON 解析失败: " + e.getMessage());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static String safeStr(Object o, String def) {
|
||||
return o != null ? String.valueOf(o).trim() : def;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static String[] toStrList(Object o) {
|
||||
if (o instanceof List) {
|
||||
List<?> list = (List<?>) o;
|
||||
List<String> out = new ArrayList<>();
|
||||
for (Object item : list) {
|
||||
out.add(String.valueOf(item));
|
||||
}
|
||||
return out.isEmpty() ? new String[]{"未指定"} : out.toArray(new String[0]);
|
||||
}
|
||||
return new String[]{"未指定"};
|
||||
}
|
||||
|
||||
private static class IntentAnalysis {
|
||||
String coreIntent;
|
||||
String domain;
|
||||
String expectedOutput;
|
||||
String[] keyRequirements;
|
||||
String[] constraints;
|
||||
String[] keywords;
|
||||
|
||||
Map<String, Object> toMap() {
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("core_intent", coreIntent);
|
||||
m.put("domain", domain);
|
||||
m.put("expected_output", expectedOutput);
|
||||
m.put("key_requirements", keyRequirements != null ? Arrays.asList(keyRequirements) : new ArrayList<>());
|
||||
m.put("constraints", constraints != null ? Arrays.asList(constraints) : new ArrayList<>());
|
||||
m.put("keywords", keywords != null ? Arrays.asList(keywords) : new ArrayList<>());
|
||||
return m;
|
||||
}
|
||||
|
||||
ExpertGenerate3Response.IntentAnalysis toIntentAnalysis() {
|
||||
ExpertGenerate3Response.IntentAnalysis out = new ExpertGenerate3Response.IntentAnalysis();
|
||||
out.setCoreIntent(coreIntent);
|
||||
out.setDomain(domain);
|
||||
out.setExpectedOutput(expectedOutput);
|
||||
out.setKeyRequirements(keyRequirements != null ? keyRequirements : new String[]{"未指定"});
|
||||
out.setConstraints(constraints != null ? constraints : new String[]{"未指定"});
|
||||
return out;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,14 +12,27 @@
|
||||
android:orientation="vertical"
|
||||
android:padding="16dp">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="提示词智能优化"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#1E3A8A"
|
||||
android:layout_marginBottom="16dp" />
|
||||
android:orientation="horizontal"
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="16dp">
|
||||
<TextView
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:text="提示词智能优化(本地)"
|
||||
android:textSize="22sp"
|
||||
android:textStyle="bold"
|
||||
android:textColor="#1E3A8A" />
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSettings"
|
||||
style="@style/Widget.Material3.Button.TextButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="设置" />
|
||||
</LinearLayout>
|
||||
|
||||
<com.google.android.material.card.MaterialCardView
|
||||
android:layout_width="match_parent"
|
||||
|
||||
65
exampleAiApp01/app/src/main/res/layout/activity_settings.xml
Normal file
65
exampleAiApp01/app/src/main/res/layout/activity_settings.xml
Normal file
@@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:padding="16dp">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="本地模型配置"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"
|
||||
android:layout_marginBottom="24dp" />
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="API 地址(Ollama 默认 http://10.0.2.2:11434/v1/)"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_marginBottom="16dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etBaseUrl"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="textUri" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.textfield.TextInputLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="模型名称(如 qwen2:0.5b、llama2)"
|
||||
style="@style/Widget.Material3.TextInputLayout.OutlinedBox"
|
||||
android:layout_marginBottom="24dp">
|
||||
|
||||
<com.google.android.material.textfield.TextInputEditText
|
||||
android:id="@+id/etModel"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text" />
|
||||
</com.google.android.material.textfield.TextInputLayout>
|
||||
|
||||
<com.google.android.material.button.MaterialButton
|
||||
android:id="@+id/btnSave"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="保存" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="24dp"
|
||||
android:text="说明:\n1. 请先在电脑安装 Ollama,运行 ollama run qwen2:0.5b\n2. 模拟器访问宿主机用 10.0.2.2,真机用电脑局域网 IP\n3. 例如:http://192.168.1.100:11434/v1/"
|
||||
android:textSize="12sp"
|
||||
android:textColor="#666666"
|
||||
android:lineSpacingExtra="4dp" />
|
||||
|
||||
</LinearLayout>
|
||||
</ScrollView>
|
||||
BIN
exampleAiApp01/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
BIN
exampleAiApp01/gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
172
exampleAiApp01/gradlew
vendored
Executable file
172
exampleAiApp01/gradlew
vendored
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/usr/bin/env sh
|
||||
|
||||
##############################################################################
|
||||
##
|
||||
## Gradle start up script for UN*X
|
||||
##
|
||||
##############################################################################
|
||||
|
||||
# Attempt to set APP_HOME
|
||||
# Resolve links: $0 may be a link
|
||||
PRG="$0"
|
||||
# Need this for relative symlinks.
|
||||
while [ -h "$PRG" ] ; do
|
||||
ls=`ls -ld "$PRG"`
|
||||
link=`expr "$ls" : '.*-> \(.*\)$'`
|
||||
if expr "$link" : '/.*' > /dev/null; then
|
||||
PRG="$link"
|
||||
else
|
||||
PRG=`dirname "$PRG"`"/$link"
|
||||
fi
|
||||
done
|
||||
SAVED="`pwd`"
|
||||
cd "`dirname \"$PRG\"`/" >/dev/null
|
||||
APP_HOME="`pwd -P`"
|
||||
cd "$SAVED" >/dev/null
|
||||
|
||||
APP_NAME="Gradle"
|
||||
APP_BASE_NAME=`basename "$0"`
|
||||
|
||||
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
|
||||
DEFAULT_JVM_OPTS=""
|
||||
|
||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||
MAX_FD="maximum"
|
||||
|
||||
warn () {
|
||||
echo "$*"
|
||||
}
|
||||
|
||||
die () {
|
||||
echo
|
||||
echo "$*"
|
||||
echo
|
||||
exit 1
|
||||
}
|
||||
|
||||
# OS specific support (must be 'true' or 'false').
|
||||
cygwin=false
|
||||
msys=false
|
||||
darwin=false
|
||||
nonstop=false
|
||||
case "`uname`" in
|
||||
CYGWIN* )
|
||||
cygwin=true
|
||||
;;
|
||||
Darwin* )
|
||||
darwin=true
|
||||
;;
|
||||
MINGW* )
|
||||
msys=true
|
||||
;;
|
||||
NONSTOP* )
|
||||
nonstop=true
|
||||
;;
|
||||
esac
|
||||
|
||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# Determine the Java command to use to start the JVM.
|
||||
if [ -n "$JAVA_HOME" ] ; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
fi
|
||||
if [ ! -x "$JAVACMD" ] ; then
|
||||
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
else
|
||||
JAVACMD="java"
|
||||
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
|
||||
|
||||
Please set the JAVA_HOME variable in your environment to match the
|
||||
location of your Java installation."
|
||||
fi
|
||||
|
||||
# Increase the maximum file descriptors if we can.
|
||||
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
|
||||
MAX_FD_LIMIT=`ulimit -H -n`
|
||||
if [ $? -eq 0 ] ; then
|
||||
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
|
||||
MAX_FD="$MAX_FD_LIMIT"
|
||||
fi
|
||||
ulimit -n $MAX_FD
|
||||
if [ $? -ne 0 ] ; then
|
||||
warn "Could not set maximum file descriptor limit: $MAX_FD"
|
||||
fi
|
||||
else
|
||||
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
|
||||
fi
|
||||
fi
|
||||
|
||||
# For Darwin, add options to specify how the application appears in the dock
|
||||
if $darwin; then
|
||||
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
|
||||
fi
|
||||
|
||||
# For Cygwin, switch paths to Windows format before running java
|
||||
if $cygwin ; then
|
||||
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
|
||||
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
|
||||
JAVACMD=`cygpath --unix "$JAVACMD"`
|
||||
|
||||
# We build the pattern for arguments to be converted via cygpath
|
||||
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
|
||||
SEP=""
|
||||
for dir in $ROOTDIRSRAW ; do
|
||||
ROOTDIRS="$ROOTDIRS$SEP$dir"
|
||||
SEP="|"
|
||||
done
|
||||
OURCYGPATTERN="(^($ROOTDIRS))"
|
||||
# Add a user-defined pattern to the cygpath arguments
|
||||
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
|
||||
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
|
||||
fi
|
||||
# Now convert the arguments - kludge to limit ourselves to /bin/sh
|
||||
i=0
|
||||
for arg in "$@" ; do
|
||||
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
|
||||
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
|
||||
|
||||
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
|
||||
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
|
||||
else
|
||||
eval `echo args$i`="\"$arg\""
|
||||
fi
|
||||
i=$((i+1))
|
||||
done
|
||||
case $i in
|
||||
(0) set -- ;;
|
||||
(1) set -- "$args0" ;;
|
||||
(2) set -- "$args0" "$args1" ;;
|
||||
(3) set -- "$args0" "$args1" "$args2" ;;
|
||||
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
|
||||
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
|
||||
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
|
||||
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
|
||||
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
|
||||
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
|
||||
esac
|
||||
fi
|
||||
|
||||
# Escape application args
|
||||
save () {
|
||||
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
|
||||
echo " "
|
||||
}
|
||||
APP_ARGS=$(save "$@")
|
||||
|
||||
# Collect all arguments for the java command, following the shell quoting and substitution rules
|
||||
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
|
||||
|
||||
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
|
||||
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
|
||||
cd "$(dirname "$0")"
|
||||
fi
|
||||
|
||||
exec "$JAVACMD" "$@"
|
||||
@@ -10661,3 +10661,4 @@ werkzeug.routing.exceptions.BuildError: Could not build url for endpoint 'expert
|
||||
[2026-03-01 23:45:03 +0800] [28566] [INFO] Booting worker with pid: 28566
|
||||
[2026-03-01 23:45:03 +0800] [28566] [INFO] 工作进程 28566 已启动
|
||||
[2026-03-01 23:45:03 +0800] [28566] [INFO] 工作进程 28566 初始化完成
|
||||
[2026-03-02 00:11:37 +0800] [28391] [INFO] Handling signal: winch
|
||||
|
||||
Reference in New Issue
Block a user