[AC-LLM-MULTI] feat(llm): 实现 LLM 多用途配置功能
- 新增 LLMUsageType 枚举支持 chat 和 kb_processing 两种用途 - 扩展 LLMConfig 支持按用途类型存储不同配置 - 更新 LLMClient 接口支持 Any 类型的消息内容 - 新增管理后台 API 支持获取用途类型列表和按用途获取配置 - 更新前端 LLM 配置页面支持多用途配置切换
|
|
@ -6,7 +6,9 @@ import type {
|
||||||
LLMTestResult,
|
LLMTestResult,
|
||||||
LLMTestRequest,
|
LLMTestRequest,
|
||||||
LLMProvidersResponse,
|
LLMProvidersResponse,
|
||||||
LLMConfigUpdateResponse
|
LLMUsageTypesResponse,
|
||||||
|
LLMConfigUpdateResponse,
|
||||||
|
LLMAllConfigs
|
||||||
} from '@/types/llm'
|
} from '@/types/llm'
|
||||||
|
|
||||||
export function getLLMProviders(): Promise<LLMProvidersResponse> {
|
export function getLLMProviders(): Promise<LLMProvidersResponse> {
|
||||||
|
|
@ -16,10 +18,22 @@ export function getLLMProviders(): Promise<LLMProvidersResponse> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getLLMConfig(): Promise<LLMConfig> {
|
export function getLLMUsageTypes(): Promise<LLMUsageTypesResponse> {
|
||||||
|
return request({
|
||||||
|
url: '/admin/llm/usage-types',
|
||||||
|
method: 'get'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getLLMConfig(usageType?: string): Promise<LLMConfig | LLMAllConfigs> {
|
||||||
|
const params: Record<string, string> = {}
|
||||||
|
if (usageType) {
|
||||||
|
params.usage_type = usageType
|
||||||
|
}
|
||||||
return request({
|
return request({
|
||||||
url: '/admin/llm/config',
|
url: '/admin/llm/config',
|
||||||
method: 'get'
|
method: 'get',
|
||||||
|
params
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,5 +60,7 @@ export type {
|
||||||
LLMTestResult,
|
LLMTestResult,
|
||||||
LLMTestRequest,
|
LLMTestRequest,
|
||||||
LLMProvidersResponse,
|
LLMProvidersResponse,
|
||||||
LLMConfigUpdateResponse
|
LLMUsageTypesResponse,
|
||||||
|
LLMConfigUpdateResponse,
|
||||||
|
LLMAllConfigs
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -2,26 +2,40 @@ import { defineStore } from 'pinia'
|
||||||
import { ref, computed } from 'vue'
|
import { ref, computed } from 'vue'
|
||||||
import {
|
import {
|
||||||
getLLMProviders,
|
getLLMProviders,
|
||||||
|
getLLMUsageTypes,
|
||||||
getLLMConfig,
|
getLLMConfig,
|
||||||
updateLLMConfig,
|
updateLLMConfig,
|
||||||
testLLM,
|
testLLM,
|
||||||
type LLMProviderInfo,
|
type LLMProviderInfo,
|
||||||
type LLMConfig,
|
type LLMConfig,
|
||||||
type LLMConfigUpdate,
|
type LLMConfigUpdate,
|
||||||
type LLMTestResult
|
type LLMTestResult,
|
||||||
|
type LLMUsageType,
|
||||||
|
type LLMAllConfigs
|
||||||
} from '@/api/llm'
|
} from '@/api/llm'
|
||||||
|
|
||||||
export const useLLMStore = defineStore('llm', () => {
|
export const useLLMStore = defineStore('llm', () => {
|
||||||
const providers = ref<LLMProviderInfo[]>([])
|
const providers = ref<LLMProviderInfo[]>([])
|
||||||
const currentConfig = ref<LLMConfig>({
|
const usageTypes = ref<LLMUsageType[]>([])
|
||||||
provider: '',
|
const allConfigs = ref<LLMAllConfigs>({
|
||||||
config: {}
|
chat: { provider: '', config: {} },
|
||||||
|
kb_processing: { provider: '', config: {} }
|
||||||
})
|
})
|
||||||
|
const currentUsageType = ref<string>('chat')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const providersLoading = ref(false)
|
const providersLoading = ref(false)
|
||||||
const testResult = ref<LLMTestResult | null>(null)
|
const testResult = ref<LLMTestResult | null>(null)
|
||||||
const testLoading = ref(false)
|
const testLoading = ref(false)
|
||||||
|
|
||||||
|
const currentConfig = computed(() => {
|
||||||
|
const config = allConfigs.value[currentUsageType.value as keyof LLMAllConfigs]
|
||||||
|
return {
|
||||||
|
provider: config?.provider || '',
|
||||||
|
config: config?.config || {},
|
||||||
|
usage_type: currentUsageType.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
const currentProvider = computed(() => {
|
const currentProvider = computed(() => {
|
||||||
return providers.value.find(p => p.name === currentConfig.value.provider)
|
return providers.value.find(p => p.name === currentConfig.value.provider)
|
||||||
})
|
})
|
||||||
|
|
@ -43,16 +57,29 @@ export const useLLMStore = defineStore('llm', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const loadUsageTypes = async () => {
|
||||||
|
try {
|
||||||
|
const res: any = await getLLMUsageTypes()
|
||||||
|
usageTypes.value = res?.usage_types || res?.data?.usage_types || []
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load LLM usage types:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
const res: any = await getLLMConfig()
|
const res: any = await getLLMConfig()
|
||||||
const config = res?.data || res
|
const configs = res?.data || res
|
||||||
if (config) {
|
if (configs) {
|
||||||
currentConfig.value = {
|
if (configs.chat && configs.kb_processing) {
|
||||||
provider: config.provider || '',
|
allConfigs.value = configs
|
||||||
config: config.config || {},
|
} else {
|
||||||
updated_at: config.updated_at
|
allConfigs.value = {
|
||||||
|
chat: { provider: configs.provider || '', config: configs.config || {} },
|
||||||
|
kb_processing: { provider: configs.provider || '', config: configs.config || {} }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -68,7 +95,8 @@ export const useLLMStore = defineStore('llm', () => {
|
||||||
try {
|
try {
|
||||||
const updateData: LLMConfigUpdate = {
|
const updateData: LLMConfigUpdate = {
|
||||||
provider: currentConfig.value.provider,
|
provider: currentConfig.value.provider,
|
||||||
config: currentConfig.value.config
|
config: currentConfig.value.config,
|
||||||
|
usage_type: currentUsageType.value
|
||||||
}
|
}
|
||||||
await updateLLMConfig(updateData)
|
await updateLLMConfig(updateData)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -86,7 +114,8 @@ export const useLLMStore = defineStore('llm', () => {
|
||||||
const result = await testLLM({
|
const result = await testLLM({
|
||||||
test_prompt: testPrompt,
|
test_prompt: testPrompt,
|
||||||
provider: currentConfig.value.provider,
|
provider: currentConfig.value.provider,
|
||||||
config: currentConfig.value.config
|
config: currentConfig.value.config,
|
||||||
|
usage_type: currentUsageType.value
|
||||||
})
|
})
|
||||||
testResult.value = result
|
testResult.value = result
|
||||||
return result
|
return result
|
||||||
|
|
@ -103,7 +132,8 @@ export const useLLMStore = defineStore('llm', () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const setProvider = (providerName: string) => {
|
const setProvider = (providerName: string) => {
|
||||||
currentConfig.value.provider = providerName
|
const usageTypeKey = currentUsageType.value as keyof LLMAllConfigs
|
||||||
|
allConfigs.value[usageTypeKey].provider = providerName
|
||||||
const provider = providers.value.find(p => p.name === providerName)
|
const provider = providers.value.find(p => p.name === providerName)
|
||||||
if (provider?.config_schema?.properties) {
|
if (provider?.config_schema?.properties) {
|
||||||
const newConfig: Record<string, any> = {}
|
const newConfig: Record<string, any> = {}
|
||||||
|
|
@ -127,14 +157,19 @@ export const useLLMStore = defineStore('llm', () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
currentConfig.value.config = newConfig
|
allConfigs.value[usageTypeKey].config = newConfig
|
||||||
} else {
|
} else {
|
||||||
currentConfig.value.config = {}
|
allConfigs.value[usageTypeKey].config = {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateConfigValue = (key: string, value: any) => {
|
const updateConfigValue = (key: string, value: any) => {
|
||||||
currentConfig.value.config[key] = value
|
const usageTypeKey = currentUsageType.value as keyof LLMAllConfigs
|
||||||
|
allConfigs.value[usageTypeKey].config[key] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
const setCurrentUsageType = (usageType: string) => {
|
||||||
|
currentUsageType.value = usageType
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearTestResult = () => {
|
const clearTestResult = () => {
|
||||||
|
|
@ -143,6 +178,9 @@ export const useLLMStore = defineStore('llm', () => {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
providers,
|
providers,
|
||||||
|
usageTypes,
|
||||||
|
allConfigs,
|
||||||
|
currentUsageType,
|
||||||
currentConfig,
|
currentConfig,
|
||||||
loading,
|
loading,
|
||||||
providersLoading,
|
providersLoading,
|
||||||
|
|
@ -151,11 +189,13 @@ export const useLLMStore = defineStore('llm', () => {
|
||||||
currentProvider,
|
currentProvider,
|
||||||
configSchema,
|
configSchema,
|
||||||
loadProviders,
|
loadProviders,
|
||||||
|
loadUsageTypes,
|
||||||
loadConfig,
|
loadConfig,
|
||||||
saveCurrentConfig,
|
saveCurrentConfig,
|
||||||
runTest,
|
runTest,
|
||||||
setProvider,
|
setProvider,
|
||||||
updateConfigValue,
|
updateConfigValue,
|
||||||
|
setCurrentUsageType,
|
||||||
clearTestResult
|
clearTestResult
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -5,15 +5,32 @@ export interface LLMProviderInfo {
|
||||||
config_schema: Record<string, any>
|
config_schema: Record<string, any>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LLMUsageType {
|
||||||
|
name: string
|
||||||
|
display_name: string
|
||||||
|
description: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface LLMConfig {
|
export interface LLMConfig {
|
||||||
provider: string
|
provider: string
|
||||||
config: Record<string, any>
|
config: Record<string, any>
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LLMConfigByUsage {
|
||||||
|
provider: string
|
||||||
|
config: Record<string, any>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LLMAllConfigs {
|
||||||
|
chat: LLMConfigByUsage
|
||||||
|
kb_processing: LLMConfigByUsage
|
||||||
|
}
|
||||||
|
|
||||||
export interface LLMConfigUpdate {
|
export interface LLMConfigUpdate {
|
||||||
provider: string
|
provider: string
|
||||||
config?: Record<string, any>
|
config?: Record<string, any>
|
||||||
|
usage_type?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LLMTestResult {
|
export interface LLMTestResult {
|
||||||
|
|
@ -31,12 +48,17 @@ export interface LLMTestRequest {
|
||||||
test_prompt?: string
|
test_prompt?: string
|
||||||
provider?: string
|
provider?: string
|
||||||
config?: Record<string, any>
|
config?: Record<string, any>
|
||||||
|
usage_type?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LLMProvidersResponse {
|
export interface LLMProvidersResponse {
|
||||||
providers: LLMProviderInfo[]
|
providers: LLMProviderInfo[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LLMUsageTypesResponse {
|
||||||
|
usage_types: LLMUsageType[]
|
||||||
|
}
|
||||||
|
|
||||||
export interface LLMConfigUpdateResponse {
|
export interface LLMConfigUpdateResponse {
|
||||||
success: boolean
|
success: boolean
|
||||||
message: string
|
message: string
|
||||||
|
|
|
||||||
|
|
@ -4,13 +4,7 @@
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<div class="title-section">
|
<div class="title-section">
|
||||||
<h1 class="page-title">LLM 模型配置</h1>
|
<h1 class="page-title">LLM 模型配置</h1>
|
||||||
<p class="page-desc">配置和管理系统使用的大语言模型,支持多种提供者切换。配置修改后需保存才能生效。</p>
|
<p class="page-desc">配置和管理系统使用的大语言模型,支持多种提供者切换。可以为不同用途配置不同的模型。</p>
|
||||||
</div>
|
|
||||||
<div class="header-actions" v-if="currentConfig.updated_at">
|
|
||||||
<div class="update-info">
|
|
||||||
<el-icon class="update-icon"><Clock /></el-icon>
|
|
||||||
<span>上次更新: {{ formatDate(currentConfig.updated_at) }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -30,6 +24,26 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
|
<div class="usage-type-section">
|
||||||
|
<div class="section-label">
|
||||||
|
<el-icon><Setting /></el-icon>
|
||||||
|
<span>选择用途类型</span>
|
||||||
|
</div>
|
||||||
|
<el-radio-group v-model="currentUsageType" class="usage-type-radio" @change="handleUsageTypeChange">
|
||||||
|
<el-radio-button
|
||||||
|
v-for="ut in usageTypes"
|
||||||
|
:key="ut.name"
|
||||||
|
:value="ut.name"
|
||||||
|
>
|
||||||
|
<el-tooltip :content="ut.description" placement="top">
|
||||||
|
<span>{{ ut.display_name }}</span>
|
||||||
|
</el-tooltip>
|
||||||
|
</el-radio-button>
|
||||||
|
</el-radio-group>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-divider />
|
||||||
|
|
||||||
<div class="provider-select-section">
|
<div class="provider-select-section">
|
||||||
<div class="section-label">
|
<div class="section-label">
|
||||||
<el-icon><Connection /></el-icon>
|
<el-icon><Connection /></el-icon>
|
||||||
|
|
@ -103,7 +117,7 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Cpu, Connection, InfoFilled, Box, RefreshLeft, Check, Clock } from '@element-plus/icons-vue'
|
import { Cpu, Connection, InfoFilled, Box, RefreshLeft, Check, Setting } from '@element-plus/icons-vue'
|
||||||
import { useLLMStore } from '@/stores/llm'
|
import { useLLMStore } from '@/stores/llm'
|
||||||
import ProviderSelect from '@/components/common/ProviderSelect.vue'
|
import ProviderSelect from '@/components/common/ProviderSelect.vue'
|
||||||
import ConfigForm from '@/components/common/ConfigForm.vue'
|
import ConfigForm from '@/components/common/ConfigForm.vue'
|
||||||
|
|
@ -117,21 +131,18 @@ const saving = ref(false)
|
||||||
const pageLoading = ref(false)
|
const pageLoading = ref(false)
|
||||||
|
|
||||||
const providers = computed(() => llmStore.providers)
|
const providers = computed(() => llmStore.providers)
|
||||||
|
const usageTypes = computed(() => llmStore.usageTypes)
|
||||||
const currentConfig = computed(() => llmStore.currentConfig)
|
const currentConfig = computed(() => llmStore.currentConfig)
|
||||||
const currentProvider = computed(() => llmStore.currentProvider)
|
const currentProvider = computed(() => llmStore.currentProvider)
|
||||||
const configSchema = computed(() => llmStore.configSchema)
|
const configSchema = computed(() => llmStore.configSchema)
|
||||||
const providersLoading = computed(() => llmStore.providersLoading)
|
const providersLoading = computed(() => llmStore.providersLoading)
|
||||||
|
const currentUsageType = computed({
|
||||||
|
get: () => llmStore.currentUsageType,
|
||||||
|
set: (val) => llmStore.setCurrentUsageType(val)
|
||||||
|
})
|
||||||
|
|
||||||
const formatDate = (dateStr: string) => {
|
const handleUsageTypeChange = (usageType: string) => {
|
||||||
if (!dateStr) return ''
|
llmStore.setCurrentUsageType(usageType)
|
||||||
const date = new Date(dateStr)
|
|
||||||
return date.toLocaleString('zh-CN', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: '2-digit',
|
|
||||||
day: '2-digit',
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleProviderChange = (provider: any) => {
|
const handleProviderChange = (provider: any) => {
|
||||||
|
|
@ -189,6 +200,7 @@ const initPage = async () => {
|
||||||
try {
|
try {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
llmStore.loadProviders(),
|
llmStore.loadProviders(),
|
||||||
|
llmStore.loadUsageTypes(),
|
||||||
llmStore.loadConfig()
|
llmStore.loadConfig()
|
||||||
])
|
])
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
@ -253,27 +265,6 @@ onMounted(() => {
|
||||||
line-height: 1.6;
|
line-height: 1.6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-actions {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
padding: 8px 14px;
|
|
||||||
background-color: var(--bg-tertiary);
|
|
||||||
border-radius: 8px;
|
|
||||||
font-size: 13px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.update-icon {
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-tertiary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.config-card {
|
.config-card {
|
||||||
animation: fadeInUp 0.5s ease-out;
|
animation: fadeInUp 0.5s ease-out;
|
||||||
}
|
}
|
||||||
|
|
@ -329,8 +320,18 @@ onMounted(() => {
|
||||||
padding: 8px 0;
|
padding: 8px 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.provider-select-section {
|
.usage-type-section {
|
||||||
margin-bottom: 16px;
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-type-radio {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.usage-type-radio :deep(.el-radio-button__inner) {
|
||||||
|
padding: 10px 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.section-label {
|
.section-label {
|
||||||
|
|
@ -347,6 +348,10 @@ onMounted(() => {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.provider-select-section {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
.provider-info {
|
.provider-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ from fastapi import APIRouter, Depends, Header, HTTPException
|
||||||
|
|
||||||
from app.services.llm.factory import (
|
from app.services.llm.factory import (
|
||||||
LLMProviderFactory,
|
LLMProviderFactory,
|
||||||
|
LLMUsageType,
|
||||||
|
LLM_USAGE_DISPLAY_NAMES,
|
||||||
|
LLM_USAGE_DESCRIPTIONS,
|
||||||
get_llm_config_manager,
|
get_llm_config_manager,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -49,25 +52,63 @@ async def list_providers(
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/usage-types")
|
||||||
|
async def list_usage_types(
|
||||||
|
tenant_id: str = Depends(get_tenant_id),
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
List all available LLM usage types.
|
||||||
|
"""
|
||||||
|
logger.info(f"Listing LLM usage types for tenant={tenant_id}")
|
||||||
|
|
||||||
|
return {
|
||||||
|
"usage_types": [
|
||||||
|
{
|
||||||
|
"name": ut.value,
|
||||||
|
"display_name": LLM_USAGE_DISPLAY_NAMES[ut],
|
||||||
|
"description": LLM_USAGE_DESCRIPTIONS[ut],
|
||||||
|
}
|
||||||
|
for ut in LLMUsageType
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/config")
|
@router.get("/config")
|
||||||
async def get_config(
|
async def get_config(
|
||||||
tenant_id: str = Depends(get_tenant_id),
|
tenant_id: str = Depends(get_tenant_id),
|
||||||
|
usage_type: str | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Get current LLM configuration.
|
Get current LLM configuration.
|
||||||
[AC-ASA-14] Returns current provider and config.
|
[AC-ASA-14] Returns current provider and config.
|
||||||
|
If usage_type is specified, returns config for that usage type.
|
||||||
|
Otherwise, returns all configs.
|
||||||
"""
|
"""
|
||||||
logger.info(f"[AC-ASA-14] Getting LLM config for tenant={tenant_id}")
|
logger.info(f"[AC-ASA-14] Getting LLM config for tenant={tenant_id}, usage_type={usage_type}")
|
||||||
|
|
||||||
manager = get_llm_config_manager()
|
manager = get_llm_config_manager()
|
||||||
config = manager.get_current_config()
|
|
||||||
|
|
||||||
masked_config = _mask_secrets(config.get("config", {}))
|
if usage_type:
|
||||||
|
try:
|
||||||
|
ut = LLMUsageType(usage_type)
|
||||||
|
config = manager.get_current_config(ut)
|
||||||
|
masked_config = _mask_secrets(config.get("config", {}))
|
||||||
|
return {
|
||||||
|
"usage_type": config["usage_type"],
|
||||||
|
"provider": config["provider"],
|
||||||
|
"config": masked_config,
|
||||||
|
}
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid usage_type: {usage_type}")
|
||||||
|
|
||||||
return {
|
all_configs = manager.get_current_config()
|
||||||
"provider": config["provider"],
|
result = {}
|
||||||
"config": masked_config,
|
for ut_key, config in all_configs.items():
|
||||||
}
|
result[ut_key] = {
|
||||||
|
"provider": config["provider"],
|
||||||
|
"config": _mask_secrets(config.get("config", {})),
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.put("/config")
|
@router.put("/config")
|
||||||
|
|
@ -78,11 +119,25 @@ async def update_config(
|
||||||
"""
|
"""
|
||||||
Update LLM configuration.
|
Update LLM configuration.
|
||||||
[AC-ASA-16] Updates provider and config with validation.
|
[AC-ASA-16] Updates provider and config with validation.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
- For specific usage type:
|
||||||
|
{
|
||||||
|
"usage_type": "chat" | "kb_processing",
|
||||||
|
"provider": "openai",
|
||||||
|
"config": {...}
|
||||||
|
}
|
||||||
|
- For all usage types (backward compatible):
|
||||||
|
{
|
||||||
|
"provider": "openai",
|
||||||
|
"config": {...}
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
provider = body.get("provider")
|
provider = body.get("provider")
|
||||||
config = body.get("config", {})
|
config = body.get("config", {})
|
||||||
|
usage_type_str = body.get("usage_type")
|
||||||
|
|
||||||
logger.info(f"[AC-ASA-16] Updating LLM config for tenant={tenant_id}, provider={provider}")
|
logger.info(f"[AC-ASA-16] Updating LLM config for tenant={tenant_id}, provider={provider}, usage_type={usage_type_str}")
|
||||||
|
|
||||||
if not provider:
|
if not provider:
|
||||||
return {
|
return {
|
||||||
|
|
@ -92,12 +147,24 @@ async def update_config(
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manager = get_llm_config_manager()
|
manager = get_llm_config_manager()
|
||||||
await manager.update_config(provider, config)
|
|
||||||
|
|
||||||
return {
|
if usage_type_str:
|
||||||
"success": True,
|
try:
|
||||||
"message": f"LLM configuration updated to {provider}",
|
usage_type = LLMUsageType(usage_type_str)
|
||||||
}
|
await manager.update_usage_config(usage_type, provider, config)
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"LLM configuration updated for {usage_type_str} to {provider}",
|
||||||
|
}
|
||||||
|
except ValueError:
|
||||||
|
raise HTTPException(status_code=400, detail=f"Invalid usage_type: {usage_type_str}")
|
||||||
|
else:
|
||||||
|
await manager.update_config(provider, config)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"message": f"LLM configuration updated to {provider}",
|
||||||
|
}
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
logger.error(f"[AC-ASA-16] Invalid LLM config: {e}")
|
logger.error(f"[AC-ASA-16] Invalid LLM config: {e}")
|
||||||
|
|
@ -115,23 +182,44 @@ async def test_connection(
|
||||||
"""
|
"""
|
||||||
Test LLM connection.
|
Test LLM connection.
|
||||||
[AC-ASA-17, AC-ASA-18] Tests connection and returns response.
|
[AC-ASA-17, AC-ASA-18] Tests connection and returns response.
|
||||||
|
|
||||||
|
Request body format:
|
||||||
|
{
|
||||||
|
"test_prompt": "optional test prompt",
|
||||||
|
"provider": "optional provider to test",
|
||||||
|
"config": "optional config to test",
|
||||||
|
"usage_type": "optional usage type to test"
|
||||||
|
}
|
||||||
"""
|
"""
|
||||||
body = body or {}
|
body = body or {}
|
||||||
|
|
||||||
test_prompt = body.get("test_prompt", "你好,请简单介绍一下自己。")
|
test_prompt = body.get("test_prompt", "你好,请简单介绍一下自己。")
|
||||||
provider = body.get("provider")
|
provider = body.get("provider")
|
||||||
config = body.get("config")
|
config = body.get("config")
|
||||||
|
usage_type_str = body.get("usage_type")
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"[AC-ASA-17] Testing LLM connection for tenant={tenant_id}, "
|
f"[AC-ASA-17] Testing LLM connection for tenant={tenant_id}, "
|
||||||
f"provider={provider or 'current'}"
|
f"provider={provider or 'current'}, usage_type={usage_type_str or 'default'}"
|
||||||
)
|
)
|
||||||
|
|
||||||
manager = get_llm_config_manager()
|
manager = get_llm_config_manager()
|
||||||
|
|
||||||
|
usage_type = None
|
||||||
|
if usage_type_str:
|
||||||
|
try:
|
||||||
|
usage_type = LLMUsageType(usage_type_str)
|
||||||
|
except ValueError:
|
||||||
|
return {
|
||||||
|
"success": False,
|
||||||
|
"error": f"Invalid usage_type: {usage_type_str}",
|
||||||
|
}
|
||||||
|
|
||||||
result = await manager.test_connection(
|
result = await manager.test_connection(
|
||||||
test_prompt=test_prompt,
|
test_prompt=test_prompt,
|
||||||
provider=provider,
|
provider=provider,
|
||||||
config=config,
|
config=config,
|
||||||
|
usage_type=usage_type,
|
||||||
)
|
)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
|
||||||
|
|
@ -117,7 +117,7 @@ class LLMClient(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def generate(
|
async def generate(
|
||||||
self,
|
self,
|
||||||
messages: list[dict[str, str]],
|
messages: list[dict[str, Any]],
|
||||||
config: LLMConfig | None = None,
|
config: LLMConfig | None = None,
|
||||||
tools: list[ToolDefinition] | None = None,
|
tools: list[ToolDefinition] | None = None,
|
||||||
tool_choice: str | dict[str, Any] | None = None,
|
tool_choice: str | dict[str, Any] | None = None,
|
||||||
|
|
@ -145,7 +145,7 @@ class LLMClient(ABC):
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def stream_generate(
|
async def stream_generate(
|
||||||
self,
|
self,
|
||||||
messages: list[dict[str, str]],
|
messages: list[dict[str, Any]],
|
||||||
config: LLMConfig | None = None,
|
config: LLMConfig | None = None,
|
||||||
tools: list[ToolDefinition] | None = None,
|
tools: list[ToolDefinition] | None = None,
|
||||||
tool_choice: str | dict[str, Any] | None = None,
|
tool_choice: str | dict[str, Any] | None = None,
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ Design pattern: Factory pattern for pluggable LLM providers.
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
|
from enum import Enum
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
|
|
@ -23,6 +24,23 @@ LLM_CONFIG_FILE = Path("config/llm_config.json")
|
||||||
LLM_CONFIG_REDIS_KEY = "ai_service:config:llm"
|
LLM_CONFIG_REDIS_KEY = "ai_service:config:llm"
|
||||||
|
|
||||||
|
|
||||||
|
class LLMUsageType(str, Enum):
|
||||||
|
"""LLM usage type for different scenarios."""
|
||||||
|
CHAT = "chat"
|
||||||
|
KB_PROCESSING = "kb_processing"
|
||||||
|
|
||||||
|
|
||||||
|
LLM_USAGE_DISPLAY_NAMES: dict[LLMUsageType, str] = {
|
||||||
|
LLMUsageType.CHAT: "对话模型",
|
||||||
|
LLMUsageType.KB_PROCESSING: "知识库处理模型",
|
||||||
|
}
|
||||||
|
|
||||||
|
LLM_USAGE_DESCRIPTIONS: dict[LLMUsageType, str] = {
|
||||||
|
LLMUsageType.CHAT: "用于 Agent 对话、问答等交互场景",
|
||||||
|
LLMUsageType.KB_PROCESSING: "用于知识库文档上传、元数据推断、文档处理等场景",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class LLMProviderInfo:
|
class LLMProviderInfo:
|
||||||
"""Information about an LLM provider."""
|
"""Information about an LLM provider."""
|
||||||
|
|
@ -284,6 +302,7 @@ class LLMConfigManager:
|
||||||
"""
|
"""
|
||||||
Manager for LLM configuration.
|
Manager for LLM configuration.
|
||||||
[AC-ASA-16, AC-ASA-17, AC-ASA-18] Configuration management with hot-reload and persistence.
|
[AC-ASA-16, AC-ASA-17, AC-ASA-18] Configuration management with hot-reload and persistence.
|
||||||
|
Supports multiple LLM usage types (chat, kb_processing).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self):
|
||||||
|
|
@ -293,8 +312,7 @@ class LLMConfigManager:
|
||||||
self._settings = settings
|
self._settings = settings
|
||||||
self._redis_client: redis.Redis | None = None
|
self._redis_client: redis.Redis | None = None
|
||||||
|
|
||||||
self._current_provider: str = settings.llm_provider
|
default_config = {
|
||||||
self._current_config: dict[str, Any] = {
|
|
||||||
"api_key": settings.llm_api_key,
|
"api_key": settings.llm_api_key,
|
||||||
"base_url": settings.llm_base_url,
|
"base_url": settings.llm_base_url,
|
||||||
"model": settings.llm_model,
|
"model": settings.llm_model,
|
||||||
|
|
@ -303,11 +321,42 @@ class LLMConfigManager:
|
||||||
"timeout_seconds": settings.llm_timeout_seconds,
|
"timeout_seconds": settings.llm_timeout_seconds,
|
||||||
"max_retries": settings.llm_max_retries,
|
"max_retries": settings.llm_max_retries,
|
||||||
}
|
}
|
||||||
self._client: LLMClient | None = None
|
|
||||||
|
self._configs: dict[LLMUsageType, dict[str, Any]] = {
|
||||||
|
LLMUsageType.CHAT: {
|
||||||
|
"provider": settings.llm_provider,
|
||||||
|
"config": default_config.copy(),
|
||||||
|
},
|
||||||
|
LLMUsageType.KB_PROCESSING: {
|
||||||
|
"provider": settings.llm_provider,
|
||||||
|
"config": default_config.copy(),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
self._clients: dict[LLMUsageType, LLMClient | None] = {
|
||||||
|
LLMUsageType.CHAT: None,
|
||||||
|
LLMUsageType.KB_PROCESSING: None,
|
||||||
|
}
|
||||||
|
|
||||||
self._load_from_redis()
|
self._load_from_redis()
|
||||||
self._load_from_file()
|
self._load_from_file()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chat_provider(self) -> str:
|
||||||
|
return self._configs[LLMUsageType.CHAT]["provider"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kb_processing_provider(self) -> str:
|
||||||
|
return self._configs[LLMUsageType.KB_PROCESSING]["provider"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chat_config(self) -> dict[str, Any]:
|
||||||
|
return self._configs[LLMUsageType.CHAT]["config"].copy()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def kb_processing_config(self) -> dict[str, Any]:
|
||||||
|
return self._configs[LLMUsageType.KB_PROCESSING]["config"].copy()
|
||||||
|
|
||||||
def _load_from_redis(self) -> None:
|
def _load_from_redis(self) -> None:
|
||||||
"""Load configuration from Redis if exists."""
|
"""Load configuration from Redis if exists."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -322,11 +371,19 @@ class LLMConfigManager:
|
||||||
if not saved_raw:
|
if not saved_raw:
|
||||||
return
|
return
|
||||||
saved = json.loads(saved_raw)
|
saved = json.loads(saved_raw)
|
||||||
self._current_provider = saved.get("provider", self._current_provider)
|
|
||||||
saved_config = saved.get("config", {})
|
for usage_type in LLMUsageType:
|
||||||
if saved_config:
|
type_key = usage_type.value
|
||||||
self._current_config.update(saved_config)
|
if type_key in saved:
|
||||||
logger.info(f"[AC-ASA-16] Loaded LLM config from Redis: provider={self._current_provider}")
|
self._configs[usage_type] = {
|
||||||
|
"provider": saved[type_key].get("provider", self._configs[usage_type]["provider"]),
|
||||||
|
"config": {**self._configs[usage_type]["config"], **saved[type_key].get("config", {})},
|
||||||
|
}
|
||||||
|
elif "provider" in saved:
|
||||||
|
self._configs[usage_type]["provider"] = saved.get("provider", self._configs[usage_type]["provider"])
|
||||||
|
self._configs[usage_type]["config"] = {**self._configs[usage_type]["config"], **saved.get("config", {})}
|
||||||
|
|
||||||
|
logger.info(f"[AC-ASA-16] Loaded multi-usage LLM config from Redis")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[AC-ASA-16] Failed to load LLM config from Redis: {e}")
|
logger.warning(f"[AC-ASA-16] Failed to load LLM config from Redis: {e}")
|
||||||
|
|
||||||
|
|
@ -341,50 +398,42 @@ class LLMConfigManager:
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
decode_responses=True,
|
decode_responses=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
save_data = {
|
||||||
|
usage_type.value: {
|
||||||
|
"provider": config["provider"],
|
||||||
|
"config": config["config"],
|
||||||
|
}
|
||||||
|
for usage_type, config in self._configs.items()
|
||||||
|
}
|
||||||
|
|
||||||
self._redis_client.set(
|
self._redis_client.set(
|
||||||
LLM_CONFIG_REDIS_KEY,
|
LLM_CONFIG_REDIS_KEY,
|
||||||
json.dumps({
|
json.dumps(save_data, ensure_ascii=False),
|
||||||
"provider": self._current_provider,
|
|
||||||
"config": self._current_config,
|
|
||||||
}, ensure_ascii=False),
|
|
||||||
)
|
)
|
||||||
logger.info(f"[AC-ASA-16] Saved LLM config to Redis: provider={self._current_provider}")
|
logger.info(f"[AC-ASA-16] Saved multi-usage LLM config to Redis")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[AC-ASA-16] Failed to save LLM config to Redis: {e}")
|
logger.warning(f"[AC-ASA-16] Failed to save LLM config to Redis: {e}")
|
||||||
|
|
||||||
def _load_from_redis(self) -> None:
|
|
||||||
"""Load configuration from Redis if exists."""
|
|
||||||
try:
|
|
||||||
if not self._settings.redis_enabled:
|
|
||||||
return
|
|
||||||
self._redis_client = redis.from_url(
|
|
||||||
self._settings.redis_url,
|
|
||||||
encoding="utf-8",
|
|
||||||
decode_responses=True,
|
|
||||||
)
|
|
||||||
saved_raw = self._redis_client.get(LLM_CONFIG_REDIS_KEY)
|
|
||||||
if not saved_raw:
|
|
||||||
return
|
|
||||||
saved = json.loads(saved_raw)
|
|
||||||
self._current_provider = saved.get("provider", self._current_provider)
|
|
||||||
saved_config = saved.get("config", {})
|
|
||||||
if saved_config:
|
|
||||||
self._current_config.update(saved_config)
|
|
||||||
logger.info(f"[AC-ASA-16] Loaded LLM config from Redis: provider={self._current_provider}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"[AC-ASA-16] Failed to load LLM config from Redis: {e}")
|
|
||||||
|
|
||||||
def _load_from_file(self) -> None:
|
def _load_from_file(self) -> None:
|
||||||
"""Load configuration from file if exists."""
|
"""Load configuration from file if exists."""
|
||||||
try:
|
try:
|
||||||
if LLM_CONFIG_FILE.exists():
|
if LLM_CONFIG_FILE.exists():
|
||||||
with open(LLM_CONFIG_FILE, encoding='utf-8') as f:
|
with open(LLM_CONFIG_FILE, encoding='utf-8') as f:
|
||||||
saved = json.load(f)
|
saved = json.load(f)
|
||||||
self._current_provider = saved.get("provider", self._current_provider)
|
|
||||||
saved_config = saved.get("config", {})
|
for usage_type in LLMUsageType:
|
||||||
if saved_config:
|
type_key = usage_type.value
|
||||||
self._current_config.update(saved_config)
|
if type_key in saved:
|
||||||
logger.info(f"[AC-ASA-16] Loaded LLM config from file: provider={self._current_provider}")
|
self._configs[usage_type] = {
|
||||||
|
"provider": saved[type_key].get("provider", self._configs[usage_type]["provider"]),
|
||||||
|
"config": {**self._configs[usage_type]["config"], **saved[type_key].get("config", {})},
|
||||||
|
}
|
||||||
|
elif "provider" in saved:
|
||||||
|
self._configs[usage_type]["provider"] = saved.get("provider", self._configs[usage_type]["provider"])
|
||||||
|
self._configs[usage_type]["config"] = {**self._configs[usage_type]["config"], **saved.get("config", {})}
|
||||||
|
|
||||||
|
logger.info(f"[AC-ASA-16] Loaded multi-usage LLM config from file")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"[AC-ASA-16] Failed to load LLM config from file: {e}")
|
logger.warning(f"[AC-ASA-16] Failed to load LLM config from file: {e}")
|
||||||
|
|
||||||
|
|
@ -392,26 +441,48 @@ class LLMConfigManager:
|
||||||
"""Save configuration to file."""
|
"""Save configuration to file."""
|
||||||
try:
|
try:
|
||||||
LLM_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
LLM_CONFIG_FILE.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
save_data = {
|
||||||
|
usage_type.value: {
|
||||||
|
"provider": config["provider"],
|
||||||
|
"config": config["config"],
|
||||||
|
}
|
||||||
|
for usage_type, config in self._configs.items()
|
||||||
|
}
|
||||||
|
|
||||||
with open(LLM_CONFIG_FILE, 'w', encoding='utf-8') as f:
|
with open(LLM_CONFIG_FILE, 'w', encoding='utf-8') as f:
|
||||||
json.dump({
|
json.dump(save_data, f, indent=2, ensure_ascii=False)
|
||||||
"provider": self._current_provider,
|
logger.info(f"[AC-ASA-16] Saved multi-usage LLM config to file")
|
||||||
"config": self._current_config,
|
|
||||||
}, f, indent=2, ensure_ascii=False)
|
|
||||||
logger.info(f"[AC-ASA-16] Saved LLM config to file: provider={self._current_provider}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"[AC-ASA-16] Failed to save LLM config to file: {e}")
|
logger.error(f"[AC-ASA-16] Failed to save LLM config to file: {e}")
|
||||||
|
|
||||||
def get_current_config(self) -> dict[str, Any]:
|
def get_current_config(self, usage_type: LLMUsageType | None = None) -> dict[str, Any]:
|
||||||
"""Get current LLM configuration."""
|
"""Get current LLM configuration for specified usage type or all configs."""
|
||||||
|
if usage_type:
|
||||||
|
config = self._configs.get(usage_type, self._configs[LLMUsageType.CHAT])
|
||||||
|
return {
|
||||||
|
"usage_type": usage_type.value,
|
||||||
|
"provider": config["provider"],
|
||||||
|
"config": config["config"].copy(),
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"provider": self._current_provider,
|
usage_type.value: {
|
||||||
"config": self._current_config.copy(),
|
"provider": config["provider"],
|
||||||
|
"config": config["config"].copy(),
|
||||||
|
}
|
||||||
|
for usage_type, config in self._configs.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def get_config_for_usage(self, usage_type: LLMUsageType) -> dict[str, Any]:
|
||||||
|
"""Get configuration for a specific usage type."""
|
||||||
|
return self._configs.get(usage_type, self._configs[LLMUsageType.CHAT])
|
||||||
|
|
||||||
async def update_config(
|
async def update_config(
|
||||||
self,
|
self,
|
||||||
provider: str,
|
provider: str,
|
||||||
config: dict[str, Any],
|
config: dict[str, Any],
|
||||||
|
usage_type: LLMUsageType | None = None,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
"""
|
"""
|
||||||
Update LLM configuration.
|
Update LLM configuration.
|
||||||
|
|
@ -420,6 +491,7 @@ class LLMConfigManager:
|
||||||
Args:
|
Args:
|
||||||
provider: Provider name
|
provider: Provider name
|
||||||
config: New configuration
|
config: New configuration
|
||||||
|
usage_type: Usage type to update (None = update all)
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
True if update successful
|
True if update successful
|
||||||
|
|
@ -430,17 +502,46 @@ class LLMConfigManager:
|
||||||
provider_info = LLM_PROVIDERS[provider]
|
provider_info = LLM_PROVIDERS[provider]
|
||||||
validated_config = self._validate_config(provider_info, config)
|
validated_config = self._validate_config(provider_info, config)
|
||||||
|
|
||||||
if self._client:
|
target_usage_types = [usage_type] if usage_type else list(LLMUsageType)
|
||||||
await self._client.close()
|
|
||||||
self._client = None
|
|
||||||
|
|
||||||
self._current_provider = provider
|
for ut in target_usage_types:
|
||||||
self._current_config = validated_config
|
if self._clients[ut]:
|
||||||
|
await self._clients[ut].close()
|
||||||
|
self._clients[ut] = None
|
||||||
|
|
||||||
|
self._configs[ut]["provider"] = provider
|
||||||
|
self._configs[ut]["config"] = validated_config.copy()
|
||||||
|
|
||||||
self._save_to_redis()
|
self._save_to_redis()
|
||||||
self._save_to_file()
|
self._save_to_file()
|
||||||
|
|
||||||
logger.info(f"[AC-ASA-16] LLM config updated: provider={provider}")
|
logger.info(f"[AC-ASA-16] LLM config updated: provider={provider}, usage={usage_type or 'all'}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
async def update_usage_config(
|
||||||
|
self,
|
||||||
|
usage_type: LLMUsageType,
|
||||||
|
provider: str,
|
||||||
|
config: dict[str, Any],
|
||||||
|
) -> bool:
|
||||||
|
"""Update configuration for a specific usage type."""
|
||||||
|
if provider not in LLM_PROVIDERS:
|
||||||
|
raise ValueError(f"Unsupported LLM provider: {provider}")
|
||||||
|
|
||||||
|
provider_info = LLM_PROVIDERS[provider]
|
||||||
|
validated_config = self._validate_config(provider_info, config)
|
||||||
|
|
||||||
|
if self._clients[usage_type]:
|
||||||
|
await self._clients[usage_type].close()
|
||||||
|
self._clients[usage_type] = None
|
||||||
|
|
||||||
|
self._configs[usage_type]["provider"] = provider
|
||||||
|
self._configs[usage_type]["config"] = validated_config
|
||||||
|
|
||||||
|
self._save_to_redis()
|
||||||
|
self._save_to_file()
|
||||||
|
|
||||||
|
logger.info(f"[AC-ASA-16] LLM config updated: usage={usage_type.value}, provider={provider}")
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _validate_config(
|
def _validate_config(
|
||||||
|
|
@ -462,20 +563,32 @@ class LLMConfigManager:
|
||||||
raise ValueError(f"Missing required config: {key}")
|
raise ValueError(f"Missing required config: {key}")
|
||||||
return validated
|
return validated
|
||||||
|
|
||||||
def get_client(self) -> LLMClient:
|
def get_client(self, usage_type: LLMUsageType | None = None) -> LLMClient:
|
||||||
"""Get or create LLM client with current config."""
|
"""Get or create LLM client with config for specified usage type."""
|
||||||
if self._client is None:
|
ut = usage_type or LLMUsageType.CHAT
|
||||||
self._client = LLMProviderFactory.create_client(
|
|
||||||
self._current_provider,
|
if self._clients[ut] is None:
|
||||||
self._current_config,
|
config = self._configs[ut]
|
||||||
|
self._clients[ut] = LLMProviderFactory.create_client(
|
||||||
|
config["provider"],
|
||||||
|
config["config"],
|
||||||
)
|
)
|
||||||
return self._client
|
return self._clients[ut]
|
||||||
|
|
||||||
|
def get_chat_client(self) -> LLMClient:
|
||||||
|
"""Get LLM client for chat/dialogue."""
|
||||||
|
return self.get_client(LLMUsageType.CHAT)
|
||||||
|
|
||||||
|
def get_kb_processing_client(self) -> LLMClient:
|
||||||
|
"""Get LLM client for KB processing."""
|
||||||
|
return self.get_client(LLMUsageType.KB_PROCESSING)
|
||||||
|
|
||||||
async def test_connection(
|
async def test_connection(
|
||||||
self,
|
self,
|
||||||
test_prompt: str = "你好,请简单介绍一下自己。",
|
test_prompt: str = "你好,请简单介绍一下自己。",
|
||||||
provider: str | None = None,
|
provider: str | None = None,
|
||||||
config: dict[str, Any] | None = None,
|
config: dict[str, Any] | None = None,
|
||||||
|
usage_type: LLMUsageType | None = None,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""
|
"""
|
||||||
Test LLM connection.
|
Test LLM connection.
|
||||||
|
|
@ -485,14 +598,20 @@ class LLMConfigManager:
|
||||||
test_prompt: Test prompt to send
|
test_prompt: Test prompt to send
|
||||||
provider: Optional provider to test (uses current if not specified)
|
provider: Optional provider to test (uses current if not specified)
|
||||||
config: Optional config to test (uses current if not specified)
|
config: Optional config to test (uses current if not specified)
|
||||||
|
usage_type: Usage type for config lookup
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Test result with success status, response, and metrics
|
Test result with success status, response, and metrics
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
|
|
||||||
test_provider = provider or self._current_provider
|
if usage_type and not provider:
|
||||||
test_config = config if config else self._current_config
|
usage_config = self._configs[usage_type]
|
||||||
|
test_provider = usage_config["provider"]
|
||||||
|
test_config = usage_config["config"]
|
||||||
|
else:
|
||||||
|
test_provider = provider or self._configs[LLMUsageType.CHAT]["provider"]
|
||||||
|
test_config = config if config else self._configs[LLMUsageType.CHAT]["config"]
|
||||||
|
|
||||||
logger.info(f"[AC-ASA-17] Test connection: provider={test_provider}, model={test_config.get('model')}")
|
logger.info(f"[AC-ASA-17] Test connection: provider={test_provider}, model={test_config.get('model')}")
|
||||||
|
|
||||||
|
|
@ -533,10 +652,11 @@ class LLMConfigManager:
|
||||||
}
|
}
|
||||||
|
|
||||||
async def close(self) -> None:
|
async def close(self) -> None:
|
||||||
"""Close the current client."""
|
"""Close all clients."""
|
||||||
if self._client:
|
for client in self._clients.values():
|
||||||
await self._client.close()
|
if client:
|
||||||
self._client = None
|
await client.close()
|
||||||
|
self._clients = {ut: None for ut in LLMUsageType}
|
||||||
|
|
||||||
|
|
||||||
_llm_config_manager: LLMConfigManager | None = None
|
_llm_config_manager: LLMConfigManager | None = None
|
||||||
|
|
|
||||||
|
|
@ -99,7 +99,7 @@ class OpenAIClient(LLMClient):
|
||||||
|
|
||||||
def _build_request_body(
|
def _build_request_body(
|
||||||
self,
|
self,
|
||||||
messages: list[dict[str, str]],
|
messages: list[dict[str, Any]],
|
||||||
config: LLMConfig,
|
config: LLMConfig,
|
||||||
stream: bool = False,
|
stream: bool = False,
|
||||||
tools: list[ToolDefinition] | None = None,
|
tools: list[ToolDefinition] | None = None,
|
||||||
|
|
@ -133,7 +133,7 @@ class OpenAIClient(LLMClient):
|
||||||
)
|
)
|
||||||
async def generate(
|
async def generate(
|
||||||
self,
|
self,
|
||||||
messages: list[dict[str, str]],
|
messages: list[dict[str, Any]],
|
||||||
config: LLMConfig | None = None,
|
config: LLMConfig | None = None,
|
||||||
tools: list[ToolDefinition] | None = None,
|
tools: list[ToolDefinition] | None = None,
|
||||||
tool_choice: str | dict[str, Any] | None = None,
|
tool_choice: str | dict[str, Any] | None = None,
|
||||||
|
|
@ -255,7 +255,7 @@ class OpenAIClient(LLMClient):
|
||||||
|
|
||||||
async def stream_generate(
|
async def stream_generate(
|
||||||
self,
|
self,
|
||||||
messages: list[dict[str, str]],
|
messages: list[dict[str, Any]],
|
||||||
config: LLMConfig | None = None,
|
config: LLMConfig | None = None,
|
||||||
tools: list[ToolDefinition] | None = None,
|
tools: list[ToolDefinition] | None = None,
|
||||||
tool_choice: str | dict[str, Any] | None = None,
|
tool_choice: str | dict[str, Any] | None = None,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 7年级到课赠礼
|
||||||
|
|
||||||
|
这是一张7年级到课赠礼的图片,可能包含到课奖励、学习用品或相关礼品的信息。图片可能展示赠礼的实物照片、礼品包装或相关的宣传内容。
|
||||||
|
After Width: | Height: | Size: 466 KiB |
|
|
@ -0,0 +1,25 @@
|
||||||
|
# s班小学3年级到课赠礼
|
||||||
|
|
||||||
|
这张图片是一张教育类宣传海报,主题为“飞跃领航计划”,核心内容是展示“专属福利大礼包”的五天完课福利,同时包含人物展示与视觉装饰元素。整体风格活泼,色彩明快,以浅蓝 - 浅绿渐变为主背景,搭配卡通元素增强亲和力。
|
||||||
|
|
||||||
|
## 1. 文字内容(按视觉顺序提取)
|
||||||
|
- 标题区:左上角橙色标签“3阶”,主标题“飞跃领航计划”(“飞跃”为黑色粗体,“领航计划”为绿色粗体);下方绿色横幅“专属福利大礼包”。
|
||||||
|
- 人物区:三位人物(两位女性、一位男性)并排站立,每人旁有蓝色标签标注姓名:**张婷婷**、**褚佳麟**、**王亚男**;人物下方黄色标签“思维 | 人文 | 剑桥”。
|
||||||
|
- 福利列表区(白色背景框内,按天划分):
|
||||||
|
- 第一天/完课福利:①《思维模块知识导图册》+《三年级口算14000题》
|
||||||
|
- 第二天/完课福利:①精选双语动画电影20部(上10部) ②《应用题专项练习》+《小升初数学知识要点汇总》
|
||||||
|
- 第三天/完课福利:①《知识大盘点+易错大集合》 ②《世界上下五千年》音频资料
|
||||||
|
- 第四天/完课福利:①《考前高效培优知识梳理总复习》+《期末检测卷》2套
|
||||||
|
- 第五天/完课福利:①精选双语动画电影20部(下10部)
|
||||||
|
|
||||||
|
## 2. 人物与视觉元素
|
||||||
|
- 人物:三位形象正面、微笑,穿着职业装(女性为衬衫/西装,男性为浅灰西装),姿态自然,传递专业与亲和感。
|
||||||
|
- 颜色:背景为浅蓝 - 浅绿渐变;标题文字黑、绿对比;人物标签蓝色;福利列表背景白色,文字黑色;卡通元素(底部橙子、礼物盒、小图标)色彩鲜艳(橙、黄、紫等),增加活泼感。
|
||||||
|
- 布局:顶部为标题+人物展示区,中间为福利列表(分天排版,用圆点区分项目),底部为装饰性卡通元素(如橙子、礼物、电话/书本图标),整体结构清晰,信息层级分明。
|
||||||
|
|
||||||
|
## 3. 风格与意图
|
||||||
|
海报风格偏向教育类宣传的“活泼专业”:通过卡通元素降低距离感,通过人物展示增强信任感,通过分天福利列表清晰传递“完课奖励”的核心信息,目标受众可能是中小学生或家长,旨在推广“飞跃领航计划”课程。
|
||||||
|
|
||||||
|
## 4. 额外说明
|
||||||
|
- 视觉细节:底部卡通元素(如带笑脸的橙子、礼物盒)呼应“福利礼包”主题,增强趣味性;人物标签“思维 | 人文 | 剑桥”暗示课程涵盖多学科与国际化(剑桥)特色。
|
||||||
|
- 信息逻辑:福利列表从“知识导图+口算”到“动画电影+音频”,再到“复习资料+试卷”,覆盖“知识输入 - 兴趣培养 - 复习巩固”全流程,体现课程设计的完整性。
|
||||||
|
After Width: | Height: | Size: 482 KiB |
|
After Width: | Height: | Size: 618 KiB |
|
|
@ -0,0 +1,65 @@
|
||||||
|
# 一部7年级课表
|
||||||
|
|
||||||
|
这是一张初一训练营的课程表,主题为"双语素养+科学思维",旨在帮助学生初一/初一定三年,学习不犯难。课程表包含四位主讲老师的信息和详细的课程安排,还提供了App下载二维码。
|
||||||
|
|
||||||
|
## 1. 课程主题与目标
|
||||||
|
- 主标题:训练营 飞跃领航计划
|
||||||
|
- 核心主题:双语素养+科学思维
|
||||||
|
- 目标受众:初一/初一定三年,学习不犯难
|
||||||
|
- 下载方式:扫码下载App听课(提供安卓和苹果版本二维码)
|
||||||
|
|
||||||
|
## 2. 主讲老师信息
|
||||||
|
1. **陈久杰** - 科学探究主讲
|
||||||
|
- 科学探究资深主讲老师
|
||||||
|
- 12年线上与线下教学经验
|
||||||
|
- 科学探究教学负责人
|
||||||
|
- 《中考物理满分冲刺》主编
|
||||||
|
- 人事人才网家庭教育高级指导师
|
||||||
|
- 北京大学心理发展研修班进修
|
||||||
|
|
||||||
|
2. **毕玉琦** - 卓越双语主讲
|
||||||
|
- 卓越双语学科教学总负责人和资深主讲老师
|
||||||
|
- 全国巡回英语讲座200余场
|
||||||
|
- 获新加坡南洋理工大学TESOL教学认证
|
||||||
|
- 一线英语教学10余年
|
||||||
|
- 培养中考英语高分学员10000+人
|
||||||
|
|
||||||
|
3. **阙红乾** - 思维逻辑主讲
|
||||||
|
- 中考满分或保送学员41位清北学员12位
|
||||||
|
- 12年授课经验,线上学员过百万
|
||||||
|
- 海淀数学联赛顶尖师资
|
||||||
|
- 全体教师赛课一等奖
|
||||||
|
- S级主讲、教学大师奖、最佳教学效果奖、最受学生喜爱奖、最具课程吸引力奖、中考学员王者之师、金牌培训师
|
||||||
|
|
||||||
|
4. **张晓煜** - 人文博学主讲
|
||||||
|
- 资深人文主讲、主讲培训师
|
||||||
|
- 深耕语文一线教学14年
|
||||||
|
- 高级脑力潜能开发师
|
||||||
|
- 高级家庭教育指导师
|
||||||
|
- 高级思维导图教师
|
||||||
|
- 《初中语文考点一本通》《精准练语文》《21天预复习》等图书主编
|
||||||
|
|
||||||
|
## 3. 课程安排表
|
||||||
|
|
||||||
|
| 时间 | 科目 | 课程名称 | 课程内容 | 听课要求 |
|
||||||
|
|------|------|----------|----------|----------|
|
||||||
|
| 周四 19:55-20:55 | 规划讲座 | 学习规划讲座 | 【家长必听】初一全年学习规划及高效学习方法,帮助孩子领跑新征程! | 家长必听 |
|
||||||
|
| 周五 18:55-19:55 | 思维逻辑(数) | 双中会 | 同学们对于线段双动点或者角的双角平分线理解存在较大难度,往往读不懂题。本节课帮助孩子从底层知识理解该类题目,快速搞定双角平分线或者双中点问题。 | 亲子共听 |
|
||||||
|
| 周五 19:55-20:55 | 卓越双语(英) | 有根有据记词汇 | 驼哥用词根词缀法帮大家拆解单词,用底层逻辑法教大家辨别一词多义,让词汇记忆与积累不再发愁,词汇题目迎刃而解。 | 亲子共听 |
|
||||||
|
| 周六 18:55-19:55 | 科学探究(物) | 隐力剧场:无形托举者 | 大气压强是一种看不见摸不到的物理现象,本节课运用实验让同学们在动手当中观察现象总结结论,理解物理概念。 | 亲子共听 |
|
||||||
|
| 周六 19:55-20:55 | 卓越双语(英) | 有的放矢通关语法 | 对名词和数词的分类难以掌握、名词的变形和数词的用法感到困惑。本节课将通过"武术有师父"等驼哥专属口诀帮你解决初一语法中的两大词法问题,夯实语法基础,提升10分。本节课通过讲解成长类作文结构,帮助学生掌握事件描写和开头结尾技巧,并分享高分素材,让学生写作文从流水账成长到高分作文。 | 亲子共听 |
|
||||||
|
| 周六 14:00-15:10 | 人文博学(语) | 从流水账到黄金屋 | 本章是孩子初中第一次接触几何证明题+几何辅助线构造+几何模型构造,对几何核心思维培养至关重要!本节课带你搭建几何模型思维,感受模型秒解的魅力! | 亲子共听 |
|
||||||
|
| 周日 18:55-19:55 | 思维逻辑(数) | 拐点模型 | 本章是孩子初中第一次接触几何证明题+几何辅助线构造+几何模型构造,对几何核心思维培养至关重要!本节课带你搭建几何模型思维,感受模型秒解的魅力! | 亲子共听 |
|
||||||
|
| 周日 19:55-20:55 | 科学探究(物) | 解密声音密码 | 通过实验法讲解抽象物理概念,让学生可以通透的理解声音的三大要素并能掌握核心的考点解释生活现象。启发物理思维,培养物理兴趣,为初二学习物理做好铺垫。 | 亲子共听 |
|
||||||
|
|
||||||
|
## 4. 设计特点
|
||||||
|
- 色彩:浅蓝渐变背景,搭配橙色、绿色等明亮色彩,整体风格活泼专业
|
||||||
|
- 布局:左侧为老师介绍,右侧为课程表,信息分区明确
|
||||||
|
- 互动:提供二维码下载App,方便学生和家长听课
|
||||||
|
- 听课要求:区分"家长必听"和"亲子共听",体现不同课程的参与方式
|
||||||
|
|
||||||
|
## 5. 课程特色
|
||||||
|
- 覆盖多学科:科学探究、卓越双语、思维逻辑、人文博学
|
||||||
|
- 时间安排合理:工作日晚上和周末安排课程
|
||||||
|
- 实用性强:课程内容针对初中学习重点和难点
|
||||||
|
- 方法指导:不仅教授知识,还提供学习方法指导
|
||||||
|
|
@ -0,0 +1,54 @@
|
||||||
|
# 以前缺少学习动力,高途的直播课让孩子学习态度积极;对课程老师都很认可
|
||||||
|
|
||||||
|
这是一张用户评价截图,展示了家长对孩子在高途直播课学习情况的反馈。截图呈现了对话形式,包含多位家长的评价内容,主要表达了对课程设计和老师教学的认可。
|
||||||
|
|
||||||
|
## 1. 主要评价内容
|
||||||
|
|
||||||
|
### 左侧评价(陈琦家长)
|
||||||
|
- **用户名**:陈琦(chengqi34)
|
||||||
|
- **评价内容**:
|
||||||
|
- "孩子最近状态咋样啊"
|
||||||
|
- "上课挺认真的!就是正确率仅在及格线上一点。😊"
|
||||||
|
- "你们的课程和老师都有趣!她很喜欢!😊"
|
||||||
|
- "你们的二讲老师超级负责!课后不懂的作业,老师会打电话来指导!这个很让我感动!👍👍👍"
|
||||||
|
- "也非常感谢您帮我找了三个这么优秀的二讲老师!👏👏👏"
|
||||||
|
- "现在她还没养成预习、复习、订正的习惯。"
|
||||||
|
- "如果这个习惯养成了,估计她后续的学习不会那么吃力了!上课更不会睡觉了!😊"
|
||||||
|
- "你们的课程互动环节设计得非常好!能牢牢抓住她的注意力!👍👍👍"
|
||||||
|
- "她现在上课不用我催促,非常自觉!😊"
|
||||||
|
- "也多谢您提醒我让她上午上完课就睡觉。这样她一点半上课就不会犯困了,整个下午也有精神。😊"
|
||||||
|
- "还是您懂因材施教!👍👍👍"
|
||||||
|
- **时间**:8/16 17:37:55
|
||||||
|
|
||||||
|
### 右侧评价
|
||||||
|
- **评价内容**:
|
||||||
|
- "孩子的笔记越来越好了!能看出有进步"
|
||||||
|
- "嗯嗯笔记比以前有进步,这得感谢小王老师的风趣的课堂氛围,与严格的监督与督促👍👍👍"
|
||||||
|
- "以前缺少学习动力,高途的直播课让同学对学习态度积极😊"
|
||||||
|
- "还是要看孩子练习中的问题!勇于探索与解决问题最重要啦~"
|
||||||
|
- "这孩子就是缺少学习动力,现在咱们的直播氛围比较喜欢 所以好像学习上有点积极态度了。"
|
||||||
|
- "学习本来就是很快乐的事情,没有那么难,勤奋多思考"
|
||||||
|
- "感谢您的辛苦付出🌹🌹🌹"
|
||||||
|
- "孩子的初中很好~孩子多勤奋一些,英语真不难,词汇多背,语法搞懂,多记笔记,多练习"
|
||||||
|
|
||||||
|
## 2. 视觉设计
|
||||||
|
- **布局**:左右分栏的对话形式,模拟真实的聊天界面
|
||||||
|
- **颜色**:浅灰色背景,文字为黑色,重点文字用红色突出
|
||||||
|
- **表情**:包含多种表情符号(😊、👍、👏、🌹等),增加亲和力
|
||||||
|
- **格式**:使用气泡对话框形式,模拟真实聊天场景
|
||||||
|
|
||||||
|
## 3. 评价特点
|
||||||
|
- **真实感**:采用对话形式,增强可信度
|
||||||
|
- **具体反馈**:包含具体的学习进步描述(笔记变好、上课认真等)
|
||||||
|
- **多角度评价**:从不同家长角度反映课程效果
|
||||||
|
- **情感表达**:包含感谢和积极的情感表达
|
||||||
|
- **教学认可**:特别提到老师负责、课程有趣、互动设计好等优势
|
||||||
|
|
||||||
|
## 4. 营销意图
|
||||||
|
- 展示真实用户评价,建立信任
|
||||||
|
- 突出课程对学习动力的提升作用
|
||||||
|
- 强调老师负责和课程设计优秀
|
||||||
|
- 体现因材施教的教学理念
|
||||||
|
- 展示学习进步的具体案例
|
||||||
|
|
||||||
|
这张评价截图有效地展示了高途直播课的教学效果和家长满意度,通过真实的对话形式传递课程价值。
|
||||||
|
After Width: | Height: | Size: 602 KiB |
|
|
@ -0,0 +1,51 @@
|
||||||
|
# 喜欢主讲老师,第一次上80很难得
|
||||||
|
|
||||||
|
这是一张用户评价截图,展示了家长对孩子在高途课程学习情况的反馈,特别提到了对杨易老师的喜爱和成绩提升的情况。
|
||||||
|
|
||||||
|
## 1. 主要评价内容
|
||||||
|
|
||||||
|
### 左侧评价
|
||||||
|
- **用户反馈**:
|
||||||
|
- "他是因为杨易老师了,所以杨易老师说啥他都积极响应"
|
||||||
|
- "从娃上网课的状态就能看出,他其实蛮专注力一点问题都没有。"
|
||||||
|
- "还是看老师讲的是不是他感兴趣的,上课方式是不是他喜欢的。"
|
||||||
|
- "他上课的状态太好了,我忍不住拍视频给我妈看😊"
|
||||||
|
- **系统回复**:
|
||||||
|
- "哈哈哈,这个状态太喜人啦😊"
|
||||||
|
|
||||||
|
### 右侧评价
|
||||||
|
- **用户反馈**:
|
||||||
|
- "八下第一次上了80,很难得。比之前进步很多"
|
||||||
|
- "他考试有进步的。期中考试84之前都是79左右,80分很难得"
|
||||||
|
- "哇塞,太惊喜了🌹🌹八下语文还是比较难的,取得了进步,太不错了,看得出来孩子真的投入了,努力了"
|
||||||
|
- "咱们继续保持,加油加油,我会持续关注瀚清😊"
|
||||||
|
- "我跟天翼老师也分享一下下喜报😊"
|
||||||
|
- "之前一直在其他机构,换了高途提升很多"
|
||||||
|
- "确实高途的课程适合他,也是他选择换课程的。"
|
||||||
|
- "必须分享"
|
||||||
|
- "嗯嗯,孩子也很努力,我看好孩子,后面也会越来越好的,咱们一起相互配合,一起加油😊"
|
||||||
|
- "妈妈也是第一时间被天翼老师吸引"
|
||||||
|
- "我也是第一时间被天翼老师吸引才分享给刘瀚清试课的"
|
||||||
|
|
||||||
|
## 2. 视觉设计
|
||||||
|
- **布局**:左右分栏的对话形式,模拟真实的聊天界面
|
||||||
|
- **颜色**:浅灰色背景,文字为黑色,重点文字用红色突出
|
||||||
|
- **表情**:包含多种表情符号(😊、🌹等),增加亲和力
|
||||||
|
- **格式**:使用气泡对话框形式,模拟真实聊天场景
|
||||||
|
- **图片**:左侧包含一个小图片,显示上课场景
|
||||||
|
|
||||||
|
## 3. 评价特点
|
||||||
|
- **具体成绩提升**:明确提到"八下第一次上了80",显示具体的学习进步
|
||||||
|
- **老师影响**:强调杨易老师对孩子学习状态的积极影响
|
||||||
|
- **课程转换**:提到从其他机构转到高途课程
|
||||||
|
- **家长认可**:表达对天翼老师的认可和吸引
|
||||||
|
- **真实感**:采用对话形式,增强可信度
|
||||||
|
|
||||||
|
## 4. 营销意图
|
||||||
|
- 展示真实用户评价,建立信任
|
||||||
|
- 突出老师对学生学习的积极影响
|
||||||
|
- 体现课程效果的具体案例
|
||||||
|
- 展示学生成绩提升的实例
|
||||||
|
- 强调课程转换后的积极变化
|
||||||
|
|
||||||
|
这张评价截图有效地展示了高途课程的教学效果和家长满意度,通过真实的对话形式传递课程价值,特别突出了老师对学生学习状态的积极影响。
|
||||||
|
After Width: | Height: | Size: 296 KiB |
|
|
@ -0,0 +1,54 @@
|
||||||
|
# 成绩提升30多分
|
||||||
|
|
||||||
|
这是一张用户评价截图,展示了学生在高途课程学习后取得显著成绩提升的情况,特别突出了英语成绩的进步。
|
||||||
|
|
||||||
|
## 1. 主要评价内容
|
||||||
|
|
||||||
|
### 左侧评价
|
||||||
|
- **用户反馈**:
|
||||||
|
- "老师我英语从60多进步到80多"
|
||||||
|
- "老师我们出成绩了,考的是789章的,比上一次考试进步了31分!"
|
||||||
|
- "谢谢老师夸奖"
|
||||||
|
- "这段时间我上课认真听课,单词好好背诵,然后也认真听褚帅老师的课"
|
||||||
|
- "总分从405进步到432了"
|
||||||
|
- "进步的分数里,英语占了一大半"
|
||||||
|
|
||||||
|
### 右侧评价
|
||||||
|
- **系统回复**:
|
||||||
|
- "我就知道你是可以的😊"
|
||||||
|
- "你怎么进步了,分享一下这段时间的自己规划"
|
||||||
|
- "🌹"
|
||||||
|
|
||||||
|
### 底部信息
|
||||||
|
- **成绩展示**:
|
||||||
|
- "进步30多分"(红色大字)
|
||||||
|
- 英语成绩:70分(可能为本次成绩)
|
||||||
|
- 英语成绩:66分(可能为上次成绩)
|
||||||
|
- "老师,我这次英语成绩比上次进步了30分。"
|
||||||
|
- "然后作文也有很大进步。"
|
||||||
|
- "哇塞"
|
||||||
|
|
||||||
|
## 2. 视觉元素
|
||||||
|
- **图片**:左侧包含两张图片
|
||||||
|
- 上方:学生成绩单照片
|
||||||
|
- 下方:Lays薯片卡通形象
|
||||||
|
- **卡通形象**:右侧有一个可爱的卡通小熊形象,增加亲和力
|
||||||
|
- **成绩数据**:明确显示具体的分数提升(31分,总分提升27分)
|
||||||
|
- **颜色**:浅灰色背景,文字为黑色,重点文字用红色突出
|
||||||
|
- **布局**:左右分栏的对话形式,底部有成绩展示区域
|
||||||
|
|
||||||
|
## 3. 评价特点
|
||||||
|
- **具体成绩数据**:明确提到进步31分,总分从405到432
|
||||||
|
- **学科重点**:特别强调英语成绩的提升(从60多到80多)
|
||||||
|
- **学习态度**:提到认真听课和背诵单词的良好学习习惯
|
||||||
|
- **多科进步**:不仅英语进步,作文也有很大进步
|
||||||
|
- **真实感**:包含成绩单照片,增强可信度
|
||||||
|
|
||||||
|
## 4. 营销意图
|
||||||
|
- 展示具体的成绩提升数据,证明课程效果
|
||||||
|
- 突出英语学科的显著进步
|
||||||
|
- 强调学生良好的学习态度和习惯
|
||||||
|
- 通过真实成绩单建立信任
|
||||||
|
- 展示多学科综合提升的效果
|
||||||
|
|
||||||
|
这张评价截图通过具体的成绩数据和真实的成绩单,有效地展示了高途课程对学生成绩的显著提升作用,特别是英语学科的进步。
|
||||||
|
After Width: | Height: | Size: 288 KiB |
|
|
@ -0,0 +1,56 @@
|
||||||
|
# 提升成绩
|
||||||
|
|
||||||
|
这是一张即时通讯软件的聊天记录截图,内容围绕学生与老师的对话,核心是学生成绩进步的反馈与交流。画面以聊天气泡为主要视觉元素,通过文字、表情、图片及少量图表呈现信息,整体风格偏向日常沟通,带有积极鼓励的氛围。
|
||||||
|
|
||||||
|
## 1. 文字内容(按出现顺序提取)
|
||||||
|
- 时间戳:`19:38`、`12:10`
|
||||||
|
- 对话文本:
|
||||||
|
- "老师老师,这次期中我进步了11分"
|
||||||
|
- "强"(配图,黑色背景+白色艺术字)
|
||||||
|
- "进班1个月进步11分 李琪老师太牛了!!!"
|
||||||
|
- "你太棒啦~宝"
|
||||||
|
- "安在的半期成绩"
|
||||||
|
- "进步明显"
|
||||||
|
- "感谢老师,英语有明显进步"
|
||||||
|
- "希望在老师的教导下再进步点"
|
||||||
|
- "太棒啦~继续努力呢👍"
|
||||||
|
- "麻烦妈妈吧安在同学的试卷和答题卡发给孟孟老师呢"
|
||||||
|
- "看一下本次出现的问题以及接下来的问题解决"
|
||||||
|
- "五一假期快乐呀~我看咱宝第二讲的作业还是没有完成提交,尽量在五一假期结束之前完成学习哦😊 噢噢好的写了没交我现在还在外面回去我就交"
|
||||||
|
- "老师我的数学成绩出来了"
|
||||||
|
- "86/120"(红色边框突出显示)
|
||||||
|
- "李琪老师太牛了!!! 进班不到2个月,提升42分!"
|
||||||
|
- "嘿嘿"
|
||||||
|
- "进步了是不是,我记得你进班的时候四五十分😀"
|
||||||
|
- "对呀"
|
||||||
|
- "之前43.5"(红色边框突出显示)
|
||||||
|
|
||||||
|
## 2. 视觉元素与布局
|
||||||
|
- **布局**:聊天气泡呈垂直堆叠,不同气泡颜色区分发言者(如灰色为系统/非当前用户消息,蓝色为当前用户消息,红色为强调文字/成绩)。部分关键信息(如成绩"86/120""之前43.5")用红色边框突出,增强视觉焦点。
|
||||||
|
- **颜色**:
|
||||||
|
- 聊天气泡底色:灰色(系统消息)、白色(用户消息);
|
||||||
|
- 文字颜色:黑色(常规文字)、红色(强调/成绩、感叹句);
|
||||||
|
- 表情符号:包含😊、👍、😀等,增加情感表达。
|
||||||
|
- **图表与图片**:
|
||||||
|
- 中间位置有一张小图表(疑似成绩趋势图,含折线/柱状元素),配合文字"安在的半期成绩"展示进步;
|
||||||
|
- "强"字配图(黑色背景+白色艺术字),强化"进步"的积极情绪。
|
||||||
|
- **人物与关系**:对话涉及"李琪老师""孟孟老师"(教师)与"安在""咱宝"(学生),通过"妈妈"代为传递信息,体现家校沟通场景。
|
||||||
|
|
||||||
|
## 3. 语境与信息逻辑
|
||||||
|
对话围绕**学生成绩进步**展开:学生汇报期中/半期成绩提升(如"进步11分""提升42分"),老师或家长表达鼓励("太牛了""继续努力"),同时涉及作业提交("五一假期完成学习")和后续问题解决("看本次问题及解决"),整体传递出"进步—鼓励—后续规划"的沟通逻辑。
|
||||||
|
|
||||||
|
## 4. 评价特点
|
||||||
|
- **具体成绩数据**:明确提到进步11分和42分,以及具体的分数(86/120,之前43.5)
|
||||||
|
- **老师认可**:特别提到李琪老师的优秀教学
|
||||||
|
- **多科进步**:涉及期中成绩和数学成绩
|
||||||
|
- **家校沟通**:通过妈妈传递信息,体现家校协作
|
||||||
|
- **积极氛围**:使用感叹号、表情符号和鼓励性语言
|
||||||
|
|
||||||
|
## 5. 营销意图
|
||||||
|
- 展示具体的成绩提升数据,证明课程效果
|
||||||
|
- 突出老师的教学能力
|
||||||
|
- 强调短期内的显著进步
|
||||||
|
- 展示家校合作的积极效果
|
||||||
|
- 通过真实对话建立信任
|
||||||
|
|
||||||
|
这张评价截图通过具体的成绩数据和真实的对话,有效地展示了高途课程对学生成绩的显著提升作用,特别是短期内的快速进步。
|
||||||
|
After Width: | Height: | Size: 348 KiB |
|
|
@ -0,0 +1,54 @@
|
||||||
|
# 物理拿下高分
|
||||||
|
|
||||||
|
这是一张关于"物理成绩提升"的宣传类信息图,以聊天记录和喜报形式呈现,核心内容是展示学生在物理学习上的成绩进步,突出教学效果。整体设计通过文字、表情、红色标注等元素传递积极信息,布局清晰,重点突出。
|
||||||
|
|
||||||
|
## 1. 文字内容(按位置提取)
|
||||||
|
- **顶部标题**:`物理成绩拿下高分`(红色大字体,位于页面最上方,字体加粗,视觉冲击力强)。
|
||||||
|
- **左侧红色框内聊天记录**:
|
||||||
|
- `1😊`
|
||||||
|
- `物理也挺难`
|
||||||
|
- `😊`
|
||||||
|
- **中间喜报区域**:
|
||||||
|
- `山东济南中考高分喜报!`(红色字体,突出地域和事件类型)
|
||||||
|
- `81/90分,妥妥拿下~`(红色字体,强调具体成绩)
|
||||||
|
- **右侧聊天记录**:
|
||||||
|
- `你八十几?`(浅蓝色背景,模拟聊天气泡)
|
||||||
|
- `太有石粒辣`(黑色字体,搭配哭笑表情🤣,表达惊喜)
|
||||||
|
- `老师孩子月考成绩出来了比年前期末考试成绩84分提高7分。这次91分满分是100分班级第一!孩子自从上咱物理课兴趣激情满满。还说让老师给推荐一下教辅🙏谢谢老师`(其中`84分提高7分`、`91分`、`班级第一`、`上咱物理课兴趣激情满满`被红色框标注,突出成绩提升关键信息)
|
||||||
|
- `山东济南`(红色字体,标注地域,强化地域关联)
|
||||||
|
- **下方聊天记录**:
|
||||||
|
- `刚扣9分,高分奖励这不到手了吗`
|
||||||
|
- `给你发2200学币,看看有没有可以兑换的啊`
|
||||||
|
- **页面底部**:`高途好老师`(黑色字体,标注机构/老师名称)
|
||||||
|
|
||||||
|
## 2. 视觉元素与布局
|
||||||
|
- **颜色**:
|
||||||
|
- 红色:用于标题、喜报文字、重点成绩标注,传递喜庆、强调的视觉感受;
|
||||||
|
- 黑色:用于普通聊天文字,清晰易读;
|
||||||
|
- 浅蓝色:用于聊天气泡背景,模拟真实聊天界面;
|
||||||
|
- 表情:黄色底色+蓝色眼泪的哭笑表情,增强情感表达。
|
||||||
|
- **布局**:
|
||||||
|
- 页面分为左、中、右三栏:左侧是简短聊天框,中间是核心喜报,右侧是详细成绩说明+地域标注,底部是机构名称,整体结构清晰,信息层级分明。
|
||||||
|
- **其他元素**:
|
||||||
|
- 红色边框:左侧聊天框和右侧重点成绩文字使用红色边框,引导视觉焦点;
|
||||||
|
- 聊天气泡:模拟真实对话场景,增强代入感;
|
||||||
|
- 表情符号:增加情感色彩,使内容更生动。
|
||||||
|
|
||||||
|
## 3. 语境与信息逻辑
|
||||||
|
这张图片的目的是**宣传"高途好老师"的物理辅导效果**,通过真实(或模拟)的聊天记录和成绩数据,展示学生成绩提升(从84分到91分,提高7分,班级第一),强调课程对学习兴趣的激发("兴趣激情满满"),同时通过地域标注(山东济南)和机构名称(高途好老师)强化品牌关联。设计上通过红色、重点标注等视觉手段,快速传递"成绩提升"的核心信息,吸引目标用户(学生/家长)关注。
|
||||||
|
|
||||||
|
## 4. 评价特点
|
||||||
|
- **具体成绩数据**:明确提到从84分提高到91分,提高7分,班级第一
|
||||||
|
- **学科重点**:特别突出物理学科的成绩提升
|
||||||
|
- **兴趣培养**:强调课程对学习兴趣的激发
|
||||||
|
- **地域案例**:提供山东济南的具体案例
|
||||||
|
- **奖励机制**:提到学币奖励,体现激励机制
|
||||||
|
|
||||||
|
## 5. 营销意图
|
||||||
|
- 展示具体的物理成绩提升数据,证明课程效果
|
||||||
|
- 突出老师的教学能力
|
||||||
|
- 强调学习兴趣的培养
|
||||||
|
- 提供地域性成功案例
|
||||||
|
- 展示奖励机制,增加吸引力
|
||||||
|
|
||||||
|
这张评价截图通过具体的成绩数据和真实的对话形式,有效地展示了高途物理课程对学生成绩的显著提升作用,特别是对学习兴趣的激发。
|
||||||
|
After Width: | Height: | Size: 259 KiB |
|
|
@ -0,0 +1,51 @@
|
||||||
|
# 高途让家长放心,早点认识高途就好了
|
||||||
|
|
||||||
|
这是一张用户评价截图,展示了家长对高途课程的认可和满意,表达了"早点认识高途就好了"的遗憾心情。
|
||||||
|
|
||||||
|
## 1. 主要评价内容
|
||||||
|
|
||||||
|
### 左侧评价
|
||||||
|
- **用户反馈**:
|
||||||
|
- "谢谢老师,还是老师指导有方"
|
||||||
|
- "跟着高途学,家长放心"
|
||||||
|
- "暑假跟着高途,孩子语文妈妈我就不担心了"
|
||||||
|
- "我们一起为孩子能考上重点高中而努力😊😊😊"
|
||||||
|
|
||||||
|
### 右侧评价
|
||||||
|
- **用户反馈**:
|
||||||
|
- "好的"
|
||||||
|
- "写的很好哦!"
|
||||||
|
- "表扬!希望能有所收获"
|
||||||
|
- "您太客气啦!"
|
||||||
|
- "接下来有问题都随时来问我哈"
|
||||||
|
- "早点认识高途就好了"
|
||||||
|
- "早点认识高途就好了"
|
||||||
|
- "走了不少弯路"
|
||||||
|
- "嗯嗯,慢慢来"
|
||||||
|
- "语文学习本身就是一个潜移默化的过程"
|
||||||
|
- "走了不少弯路"
|
||||||
|
- "那个抖音上那种阅读,也买过,没有系统的教学,靠娃自觉还是不行,家长又不懂"
|
||||||
|
- "嗯嗯!必须的!最后一年,拼尽全力!💪💪"
|
||||||
|
|
||||||
|
## 2. 视觉设计
|
||||||
|
- **布局**:左右分栏的对话形式,模拟真实的聊天界面
|
||||||
|
- **颜色**:浅灰色背景,文字为黑色,重点文字用红色突出
|
||||||
|
- **表情**:包含多种表情符号(😊、💪等),增加亲和力
|
||||||
|
- **格式**:使用气泡对话框形式,模拟真实聊天场景
|
||||||
|
- **重点文字**:关键语句如"跟着高途学,家长放心"和"早点认识高途就好了"用红色大字突出显示
|
||||||
|
|
||||||
|
## 3. 评价特点
|
||||||
|
- **家长认可**:明确表达对高途课程的信任和放心
|
||||||
|
- **时间紧迫感**:提到"最后一年,拼尽全力",体现中考临近的紧迫感
|
||||||
|
- **对比体验**:提到之前在其他平台学习的不足,突出高途的系统教学优势
|
||||||
|
- **遗憾情绪**:表达"早点认识高途就好了"和"走了不少弯路"的遗憾
|
||||||
|
- **积极态度**:尽管有遗憾,但仍然保持积极的学习态度
|
||||||
|
|
||||||
|
## 4. 营销意图
|
||||||
|
- 展示家长对课程的信任和满意
|
||||||
|
- 突出高途的系统教学优势
|
||||||
|
- 强调早期认识高途的重要性
|
||||||
|
- 体现中考临近的紧迫感和学习动力
|
||||||
|
- 通过真实对话建立信任
|
||||||
|
|
||||||
|
这张评价截图通过家长的真情实感,有效地展示了高途课程给家长带来的安心感和信任度,同时也暗示了早期选择高途的重要性。
|
||||||
|
After Width: | Height: | Size: 172 KiB |
|
After Width: | Height: | Size: 213 KiB |
|
|
@ -0,0 +1,66 @@
|
||||||
|
# 引导加辅导老师微信图片
|
||||||
|
|
||||||
|
这是一张操作指引图,用于指导用户在"高途"APP中查找第二讲辅导老师的微信,包含三张手机界面截图与底部的步骤说明。整体背景为暖色调橙黄色渐变,界面以白色为主,搭配橙色、红色等强调色,通过箭头指示操作流程,视觉上清晰醒目。
|
||||||
|
|
||||||
|
## 1. 整体布局与视觉风格
|
||||||
|
- **背景**:顶部至底部为橙黄色渐变,营造活泼、醒目的氛围,突出指引内容。
|
||||||
|
- **截图排列**:三张手机界面截图横向排列,每张截图通过橙色/黄色箭头指示操作逻辑,布局清晰。
|
||||||
|
|
||||||
|
## 2. 三张截图的文字与元素(从左至右)
|
||||||
|
|
||||||
|
### 左图("高途"APP首页/课程列表页)
|
||||||
|
- **顶部状态栏**:显示时间"上午10:20"、网络速度"111K/s"、电池/信号等图标。
|
||||||
|
- **"今日学习推荐"板块**:
|
||||||
|
- 课程1:"不用读全文阅读大满贯" → 主讲【新9阶】双语素养+科学思维训练营,5月2日18:55开课。
|
||||||
|
- 课程2:"大力出奇迹" → 第2讲,【新9阶】双语素养+科学思维训练营,5月2日19:56开课。
|
||||||
|
- **"全部课程"板块**:
|
||||||
|
- 课程标题:"【新9阶】双语素养+科学思维训练营" → 共7节·未学习,5月2日18:55开课。
|
||||||
|
- 主讲老师头像:王冰、韩瑞05。
|
||||||
|
- **底部导航栏**:首页、学习、**上课**(红色图标,当前选中)、发现、我的。
|
||||||
|
|
||||||
|
### 中图(课程详情页)
|
||||||
|
- **顶部状态栏**:时间"上午10:20"、网络速度"0.3K/s"。
|
||||||
|
- **课程标题**:【新9阶】双语素养+科学思维训练营 → 有效期至2024年06月03日。
|
||||||
|
- **主讲老师**:王冰、李雪冬、王泽龙(头像+"主讲老师"标签)。
|
||||||
|
- **功能入口**:学习资料、缓存课程、错题本(图标+文字)。
|
||||||
|
- **学习进度**:听课进度0%、听课时长0分。
|
||||||
|
- **"课前准备"板块**:
|
||||||
|
- 橙色按钮:"添加二讲老师微信"(核心操作入口)。
|
||||||
|
- 选项:"关注公众号收取报告" → 右侧"去关注"链接。
|
||||||
|
- **课程列表**:
|
||||||
|
- 第1讲:"不用读全文阅读大满贯" → 05月02日周四18:55 - 19:55 | 未开始。
|
||||||
|
- 第2讲:"大力出奇迹" → 05月02日周四19:56 - 21:01 | 未开始。
|
||||||
|
- 第3讲:"全等模型狂想曲"(部分可见)。
|
||||||
|
|
||||||
|
### 右图("高途成长助手"页面)
|
||||||
|
- **顶部状态栏**:时间"上午10:20"、网络速度"80.4K/s"。
|
||||||
|
- **页面标题**:高途成长助手 → 评分2.5。
|
||||||
|
- **倒计时与提示**:剩余04:56:8 → "请务必添加老师 否则无法上课"(红色警示文字)。
|
||||||
|
- **老师信息**:高途助教老师 专属(卡通头像+"专属"标签)。
|
||||||
|
- **二维码区域**:提示"长按二维码,联系老师"。
|
||||||
|
- **底部按钮**:全程辅导答疑、定制学习规划。
|
||||||
|
|
||||||
|
## 3. 底部操作步骤文字
|
||||||
|
- 第一步,下载"高途"APP
|
||||||
|
- 第二步,登录后,点击下方"上课"按钮
|
||||||
|
- 第三部,点击已报名课程("第三部"疑似"第三步"笔误)
|
||||||
|
- 第四步,点击"添加二讲老师微信"按钮,自动跳转到微信添加
|
||||||
|
|
||||||
|
## 4. 语境与信息逻辑
|
||||||
|
这张图是教育类APP"高途"的运营指引,核心目标是引导用户通过APP完成"添加第二讲辅导老师微信"的操作,确保课程学习顺利进行。三张截图对应"进入上课页面→选择课程→添加老师微信"的逻辑链,步骤简洁、视觉重点突出(如红色"上课"按钮、橙色"添加老师微信"按钮),符合用户操作习惯。暖色调背景与醒目的文字/按钮设计,提升了信息传递效率,避免用户因未添加老师而无法上课。
|
||||||
|
|
||||||
|
## 5. 设计特点
|
||||||
|
- **操作指引清晰**:通过三张截图展示完整的操作流程
|
||||||
|
- **视觉重点突出**:使用红色和橙色强调关键按钮和操作
|
||||||
|
- **信息层级分明**:不同功能区域通过颜色和布局区分
|
||||||
|
- **用户友好**:包含倒计时和警示信息,提醒用户及时操作
|
||||||
|
- **品牌展示**:展示高途APP的界面和功能
|
||||||
|
|
||||||
|
## 6. 营销意图
|
||||||
|
- 指导用户正确使用APP功能
|
||||||
|
- 确保用户能够顺利添加辅导老师
|
||||||
|
- 展示APP的用户界面和功能
|
||||||
|
- 强调添加老师的重要性
|
||||||
|
- 提升用户体验和课程参与度
|
||||||
|
|
||||||
|
这张引导图通过清晰的步骤和视觉设计,有效地指导用户完成关键操作,确保课程学习的顺利进行。
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
# 素养小学到课赠礼
|
||||||
|
|
||||||
|
这是一张小学素养课程的赠礼海报,主题为"飞跃领航计划",展示3-6阶学生的专属福利大礼包。海报设计活泼明快,以浅蓝和浅绿为主色调,包含三位主讲老师和四天的完课福利。
|
||||||
|
|
||||||
|
## 1. 课程主题与目标
|
||||||
|
- **阶段**:3-6阶(橙色标签)
|
||||||
|
- **主标题**:飞跃领航计划("飞跃"为黑色粗体,"领航计划"为绿色粗体)
|
||||||
|
- **核心主题**:专属福利大礼包(绿色横幅)
|
||||||
|
- **课程标签**:思维 | 人文 | 脑力(黄色横幅)
|
||||||
|
|
||||||
|
## 2. 主讲老师信息
|
||||||
|
海报展示了三位主讲老师:
|
||||||
|
1. **陈君** - 左侧女性老师,穿着灰色西装
|
||||||
|
2. **杨易** - 中间男性老师,穿着黑色中式服装,手持折扇
|
||||||
|
3. **白马** - 右侧男性老师,穿着深色西装
|
||||||
|
|
||||||
|
## 3. 福利列表(四天完课福利)
|
||||||
|
|
||||||
|
### 第一天/完课福利
|
||||||
|
- 《脑王数独秘籍》
|
||||||
|
|
||||||
|
### 第二天/完课福利
|
||||||
|
1. NCTE词汇表(带翻译)
|
||||||
|
2. 《世界上下五千年》音频资料(上)
|
||||||
|
|
||||||
|
### 第三天/完课福利
|
||||||
|
- 《世界上下五千年》音频资料(中)
|
||||||
|
|
||||||
|
### 第四天/完课福利
|
||||||
|
1. 《最强大脑同款》-图形推理秘籍
|
||||||
|
2. BBC自然拼读益智动画
|
||||||
|
|
||||||
|
## 4. 视觉设计
|
||||||
|
- **颜色**:浅蓝渐变背景,搭配橙色、绿色、黄色等明亮色彩
|
||||||
|
- **布局**:顶部为标题和老师展示区,中间为福利列表,底部为装饰性卡通元素
|
||||||
|
- **卡通元素**:包含可爱的橙子形象和礼物盒图标,增加活泼感
|
||||||
|
- **文字风格**:标题使用大号粗体字,福利列表使用清晰的编号和项目符号
|
||||||
|
|
||||||
|
## 5. 课程特色
|
||||||
|
- **多学科覆盖**:包含思维训练、人文知识、脑力开发
|
||||||
|
- **多样化资源**:包括书籍、音频资料、动画等不同形式的学习材料
|
||||||
|
- **循序渐进**:四天的福利安排,每天都有新的学习内容
|
||||||
|
- **趣味性**:结合《最强大脑》和BBC动画等有趣内容,增加学习吸引力
|
||||||
|
|
||||||
|
## 6. 营销意图
|
||||||
|
- 展示课程的丰富福利,吸引学生参与
|
||||||
|
- 突出多学科素养培养
|
||||||
|
- 通过具体的学习材料展示课程价值
|
||||||
|
- 强调循序渐进的学习方式
|
||||||
|
- 利用知名节目和品牌(最强大脑、BBC)增加可信度
|
||||||
|
|
||||||
|
这张海报通过清晰的结构和丰富的福利内容,有效地展示了小学素养课程的价值,特别强调了思维、人文和脑力培养的综合素养教育。
|
||||||
|
After Width: | Height: | Size: 451 KiB |
|
After Width: | Height: | Size: 270 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 8化试听-袁媛
|
||||||
|
|
||||||
|
这是一张初中化学试听课的海报图片,主讲老师是袁媛。海报应该包含课程介绍、试听时间、报名方式等相关信息。图片可能包含化学相关的图形元素、课程亮点等内容。
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 8年级_思维_佳美_数与式找规律试听课(新初一初二通用)
|
||||||
|
|
||||||
|
这是一张初中数学试听课海报,主题是"数与式找规律",适合8年级学生,由佳美老师主讲。海报应该包含数学规律题目的示例、课程特色、试听时间安排等信息。图片可能包含数学公式、几何图形或相关的教学元素。
|
||||||
|
After Width: | Height: | Size: 599 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 8数试听-YOYO游剑荷-角度模型
|
||||||
|
|
||||||
|
这是一张初中数学试听课海报,主题是"角度模型",由YOYO游剑荷老师主讲。海报应该包含角度相关的几何图形、解题方法、课程亮点等内容。图片可能展示各种角度模型的应用和示例。
|
||||||
|
After Width: | Height: | Size: 358 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 新8思维刘璐——全等三角形与辅助线
|
||||||
|
|
||||||
|
这是一张初中数学试听课海报,主题是"全等三角形与辅助线",由刘璐老师主讲。海报应该包含全等三角形的几何图形、辅助线的画法、解题技巧等内容。图片可能展示各种全等三角形的示例和辅助线的应用。
|
||||||
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 250 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 7物试听-郭志强-基础版
|
||||||
|
|
||||||
|
这是一张初中物理试听课海报,主题是基础物理知识,由郭志强老师主讲。海报应该包含物理基础概念、实验演示、课程内容介绍等信息。图片可能包含物理实验装置、公式或相关的教学元素。
|
||||||
|
After Width: | Height: | Size: 243 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 7阶试听-李雪冬-声学
|
||||||
|
|
||||||
|
这是一张初中物理试听课海报,主题是"声学",由李雪冬老师主讲。海报应该包含声音的产生、传播、特性等声学知识,以及相关的实验和演示内容。图片可能展示声学相关的图形、公式或实验装置。
|
||||||
|
After Width: | Height: | Size: 246 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 8物试听-李雪冬-光的折射
|
||||||
|
|
||||||
|
这是一张初中物理试听课海报,主题是"光的折射",由李雪冬老师主讲。海报应该包含光的折射原理、相关公式、实验演示等内容。图片可能展示光的折射现象、透镜、棱镜等光学元素。
|
||||||
|
After Width: | Height: | Size: 273 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 9物试听-电功率
|
||||||
|
|
||||||
|
这是一张初中物理试听课海报,主题是"电功率",适合9年级学生。海报应该包含电功率的计算、电路分析、相关公式等内容。图片可能展示电路图、电表、功率计算示例等元素。
|
||||||
|
After Width: | Height: | Size: 275 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 9物试听-韩盛乔-基础版
|
||||||
|
|
||||||
|
这是一张初中物理试听课海报,主题是基础物理知识,由韩盛乔老师主讲,适合9年级学生。海报应该包含物理基础概念、公式、实验等内容。图片可能展示物理实验装置、公式或相关的教学元素。
|
||||||
|
After Width: | Height: | Size: 277 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 9物试听-韩盛乔-浙教版
|
||||||
|
|
||||||
|
这是一张初中物理试听课海报,主题是浙教版物理课程,由韩盛乔老师主讲,适合9年级学生。海报应该包含浙教版物理教材的相关内容、课程特色、试听信息等。图片可能展示浙教版教材的元素或相关的教学资料。
|
||||||
|
After Width: | Height: | Size: 627 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 7英试听-张丹丹-词汇—易混词辨析
|
||||||
|
|
||||||
|
这是一张初中英语试听课海报,主题是"词汇—易混词辨析",由张丹丹老师主讲,适合7年级学生。海报应该包含英语易混词汇的对比、辨析方法、例句等内容。图片可能展示词汇对比表格、相关例句或教学元素。
|
||||||
|
After Width: | Height: | Size: 669 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 8英完形填空大招试听-张丹丹PNG
|
||||||
|
|
||||||
|
这是一张初中英语试听课海报,主题是"完形填空大招",由张丹丹老师主讲,适合8年级学生。海报应该包含完形填空解题技巧、常用方法、例题解析等内容。图片可能展示完形填空题目示例、解题步骤或相关的教学元素。
|
||||||
|
After Width: | Height: | Size: 681 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 王冰老师试听课—话题写作(难忘经历类)
|
||||||
|
|
||||||
|
这是一张初中英语试听课海报,主题是"话题写作(难忘经历类)",由王冰老师主讲。海报应该包含英语话题写作的技巧、范文示例、写作方法等内容。图片可能展示写作模板、相关例句或教学元素。
|
||||||
|
After Width: | Height: | Size: 411 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 8年级-虫子-试听
|
||||||
|
|
||||||
|
这是一张初中语文试听课海报,由虫子老师主讲,适合8年级学生。海报应该包含语文课程介绍、试听内容、教学特色等信息。图片可能展示语文相关的元素,如古诗词、文学作品或教学资料。
|
||||||
|
After Width: | Height: | Size: 398 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 写作试听课-虫子老师
|
||||||
|
|
||||||
|
这是一张初中语文试听课海报,主题是写作课程,由虫子老师主讲。海报应该包含写作技巧、范文示例、写作方法等内容。图片可能展示写作相关的元素,如作文示例、写作技巧图表或教学资料。
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 小学数学杨易
|
||||||
|
|
||||||
|
这是一张小学数学试听课海报,由杨易老师主讲。海报应该包含小学数学课程介绍、试听内容、教学特色等信息。图片可能展示数学相关的元素,如数字、图形、算式或教学资料。
|
||||||
|
After Width: | Height: | Size: 317 KiB |
|
After Width: | Height: | Size: 673 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 小学英语
|
||||||
|
|
||||||
|
这是一张小学英语试听课海报。海报应该包含小学英语课程介绍、试听内容、教学特色等信息。图片可能展示英语相关的元素,如字母、单词、简单对话或教学资料。
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 小学语文
|
||||||
|
|
||||||
|
这是一张小学语文试听课海报。海报应该包含小学语文课程介绍、试听内容、教学特色等信息。图片可能展示语文相关的元素,如汉字、词语、古诗词或教学资料。
|
||||||
|
After Width: | Height: | Size: 1.3 MiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 白杨写作试听课
|
||||||
|
|
||||||
|
这是一张小学写作试听课海报,主题是写作课程。海报应该包含写作技巧、范文示例、写作方法等内容。图片可能展示写作相关的元素,如作文示例、写作技巧图表或教学资料。
|
||||||
|
After Width: | Height: | Size: 3.8 MiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 白杨文言翻译试听课
|
||||||
|
|
||||||
|
这是一张小学文言文翻译试听课海报。海报应该包含文言文翻译技巧、经典例文、翻译方法等内容。图片可能展示文言文文本、翻译示例或相关的教学元素。
|
||||||
|
After Width: | Height: | Size: 3.7 MiB |
|
After Width: | Height: | Size: 140 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 退费引导图
|
||||||
|
|
||||||
|
这是一张退费引导的图片,可能包含退费流程、退费条件、退费申请方法等信息。图片可能展示退费步骤、相关表格或宣传内容。
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
# 高途app下载引导图
|
||||||
|
|
||||||
|
这是一张高途app下载引导的图片,可能包含app下载二维码、下载链接、安装步骤等信息。图片可能展示app界面截图、下载二维码或相关的引导内容。
|
||||||
|
After Width: | Height: | Size: 381 KiB |