Files
wasm-go/internal/api/classifier.go
lychang 7f5350db62 update
2026-05-20 03:25:35 +08:00

310 lines
8.1 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package api
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"time"
)
// CompanyIndustryClassifier 企业行业分类器
type CompanyIndustryClassifier struct {
httpClient *http.Client
llmClient *LLMClient
}
// CompanyData 企业数据
type CompanyData struct {
Description string `json:"description"`
Business string `json:"business"`
BiddingTitles []string `json:"bidding_titles"`
}
// IndustryClassification 行业分类结果
type IndustryClassification struct {
CompanyName string `json:"company_name"`
Primary PrimaryInfo `json:"primary"`
Secondary []Secondary `json:"secondary"`
}
// PrimaryInfo 主要行业信息
type PrimaryInfo struct {
Code string `json:"code"`
Name string `json:"name"`
Reason string `json:"reason"`
}
// Secondary 次要行业信息
type Secondary struct {
Code string `json:"code"`
Name string `json:"name"`
Reason string `json:"reason"`
}
// NewCompanyIndustryClassifier 创建企业行业分类器
func NewCompanyIndustryClassifier(llmClient *LLMClient) *CompanyIndustryClassifier {
client := &http.Client{
Timeout: 30 * time.Second,
}
return &CompanyIndustryClassifier{
httpClient: client,
llmClient: llmClient,
}
}
// getDescriptionText 获取简介文本
func getDescriptionText(description string) string {
if description == "" {
return "(无简介)"
}
return description
}
// getBusinessText 获取经营范围文本
func getBusinessText(business string) string {
if business == "" {
return "(无经营范围)"
}
return business
}
// FetchCompanyData 从cmdp API获取企业工商和招投标数据
func (c *CompanyIndustryClassifier) FetchCompanyData(companyName string) (map[string]interface{}, error) {
apiURL := "https://a.cmdp.cn/cmds/v1/company/info-rest/custom"
params := url.Values{}
params.Set("name", companyName)
params.Set("api", "p_info,p_biddings_all")
fullURL := apiURL + "?" + params.Encode()
req, err := http.NewRequest("GET", fullURL, nil)
if err != nil {
return nil, fmt.Errorf("创建请求失败: %v", err)
}
resp, err := c.httpClient.Do(req)
if err != nil {
return nil, fmt.Errorf("发送请求失败: %v", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("读取响应失败: %v", err)
}
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API请求失败,状态码: %d, 响应: %s", resp.StatusCode, string(body))
}
var result map[string]interface{}
err = json.Unmarshal(body, &result)
if err != nil {
return nil, fmt.Errorf("解析响应JSON失败: %v", err)
}
return result, nil
}
// ExtractFields 从API响应中提取关键字段
func (c *CompanyIndustryClassifier) ExtractFields(data map[string]interface{}) (string, string, []string) {
description := ""
business := ""
var biddingTitles []string
// 兼容不同的响应结构
payload := data
if data["data"] != nil {
if payloadData, ok := data["data"].(map[string]interface{}); ok {
payload = payloadData
}
}
// p_info -> description, business
if pInfo := payload["p_info"]; pInfo != nil {
var pInfoObj map[string]interface{}
switch v := pInfo.(type) {
case []interface{}:
if len(v) > 0 {
if item, ok := v[0].(map[string]interface{}); ok {
pInfoObj = item
}
}
case map[string]interface{}:
pInfoObj = v
}
if pInfoObj != nil {
if desc, ok := pInfoObj["description"].(string); ok {
description = desc
}
if bus, ok := pInfoObj["business"].(string); ok {
business = bus
}
}
}
// p_biddings_all -> title list
if biddingsRaw := payload["p_biddings_all"]; biddingsRaw != nil {
var biddingsList []interface{}
switch v := biddingsRaw.(type) {
case []interface{}:
biddingsList = v
case map[string]interface{}:
// 尝试从data或list字段获取
if dataField, ok := v["data"].([]interface{}); ok {
biddingsList = dataField
} else if listField, ok := v["list"].([]interface{}); ok {
biddingsList = listField
}
}
for _, item := range biddingsList {
if itemMap, ok := item.(map[string]interface{}); ok {
if title, ok := itemMap["title"].(string); ok && strings.TrimSpace(title) != "" {
biddingTitles = append(biddingTitles, strings.TrimSpace(title))
}
}
}
}
return description, business, biddingTitles
}
// BuildPrompt 构建提示词
func (c *CompanyIndustryClassifier) BuildPrompt(companyName, description, business string, biddingTitles []string) string {
titlesText := "(无招投标记录)"
if len(biddingTitles) > 0 {
var sb strings.Builder
for i, title := range biddingTitles {
sb.WriteString(fmt.Sprintf("%d. %s\n", i+1, title))
}
titlesText = sb.String()
}
prompt := fmt.Sprintf(`你是国民经济行业分类专家,请根据以下企业信息,依据《国民经济行业分类》(GB/T 4754-2017)进行行业大类判断。
## 企业名称
%s
## 企业简介(description)
%s
## 经营范围(business)
%s
## 招投标项目标题(bidding titles)
%s
## 任务要求
1. 判断该企业**主要**所属的行业大类(两位数字编号 + 名称)
2. 如有次要业务涉及其他大类,也一并列出(最多2个)
3. 给出简要判断依据(不超过100字)
## 输出格式(严格 JSON,不要 Markdown 代码块)
{
"company_name": "%s",
"primary": {
"code": "65",
"name": "软件和信息技术服务业",
"reason": "判断依据..."
},
"secondary": [
{
"code": "72",
"name": "商务服务业",
"reason": "轻微涉及原因..."
}
]
}
注意:secondary 可以为空列表 [];只输出 JSON,不要任何额外说明。`,
companyName,
getDescriptionText(description),
getBusinessText(business),
titlesText,
companyName)
return prompt
}
// ClassifyCompany 完整流程:获取数据 → 提取字段 → 构建提示词 → 调用 LLM → 返回 JSON
func (c *CompanyIndustryClassifier) ClassifyCompany(companyName string) (*IndustryClassification, error) {
// 1. 获取企业数据
rawData, err := c.FetchCompanyData(companyName)
if err != nil {
return nil, fmt.Errorf("获取企业数据失败: %v", err)
}
// 2. 提取关键字段
description, business, biddingTitles := c.ExtractFields(rawData)
// 3. 构建提示词
prompt := c.BuildPrompt(companyName, description, business, biddingTitles)
// 4. 调用LLM并解析JSON
response, err := c.llmClient.Chat([]ChatMessage{
{Role: "user", Content: prompt},
})
if err != nil {
return nil, fmt.Errorf("调用LLM失败: %v", err)
}
// 提取JSON部分(如果LLM返回了markdown代码块)
jsonStr := extractJSONFromResponse(response.Choices[0].Message.Content)
var result IndustryClassification
err = json.Unmarshal([]byte(jsonStr), &result)
if err != nil {
return nil, fmt.Errorf("解析LLM返回的JSON失败: %v", err)
}
return &result, nil
}
// extractJSONFromResponse 从LLM响应中提取JSON内容
// 查找并提取 ```json {...} ``` 或 ``` {...} ``` 中的内容
func extractJSONFromResponse(response string) string {
// 首先尝试匹配 ```json 开头的代码块
start := strings.Index(response, "```json")
if start != -1 {
start += 7 // 跳过 "```json"
end := strings.Index(response[start:], "```")
if end != -1 {
jsonContent := strings.TrimSpace(response[start : start+end])
return jsonContent
}
}
// 然后尝试匹配 ``` 开头的代码块(假设是JSON)
start = strings.Index(response, "```")
if start != -1 {
start += 3 // 跳过 "```"
end := strings.Index(response[start:], "```")
if end != -1 {
jsonContent := strings.TrimSpace(response[start : start+end])
// 检查是否看起来像JSON
jsonContent = strings.TrimSpace(jsonContent)
if strings.HasPrefix(jsonContent, "{") || strings.HasPrefix(jsonContent, "[") {
return jsonContent
}
}
}
// 如果没有找到代码块,尝试直接解析整个响应是否为JSON
response = strings.TrimSpace(response)
if strings.HasPrefix(response, "{") && strings.HasSuffix(response, "}") ||
strings.HasPrefix(response, "[") && strings.HasSuffix(response, "]") {
return response
}
// 如果以上都失败,返回原内容
return response
}