From 76b0faf39b2e121f32c4d5261cd544fd42d21bef Mon Sep 17 00:00:00 2001 From: Deng Ming Date: Sun, 1 Dec 2024 15:35:11 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E8=B7=B3=E8=BF=87=E4=BB=B7=E6=A0=BC?= =?UTF-8?q?=E4=B8=BA=200=20=E7=9A=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/ai/internal/service/llm/handler/credit/builder.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/ai/internal/service/llm/handler/credit/builder.go b/internal/ai/internal/service/llm/handler/credit/builder.go index aa03fee2..08020d72 100644 --- a/internal/ai/internal/service/llm/handler/credit/builder.go +++ b/internal/ai/internal/service/llm/handler/credit/builder.go @@ -38,6 +38,10 @@ func NewHandlerBuilder(creSvc credit.Service, repo repository.LLMCreditLogRepo) func (h *HandlerBuilder) Next(next handler.Handler) handler.Handler { return handler.HandleFunc(func(ctx context.Context, req domain.LLMRequest) (domain.LLMResponse, error) { + // 不需要扣除积分 + if req.Config.Price == 0 { + return next.Handle(ctx, req) + } cre, err := h.creditSvc.GetCreditsByUID(ctx, req.Uid) if err != nil { return domain.LLMResponse{}, err From 9e5a1c526838142b024430ca0a7d0d59d5175682 Mon Sep 17 00:00:00 2001 From: Deng Ming Date: Tue, 3 Dec 2024 17:23:08 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E5=88=86=E6=9E=90=E5=B2=97=E4=BD=8D?= =?UTF-8?q?=E7=9A=84=E6=BD=9C=E5=8F=B0=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/ai/internal/domain/jd.go | 3 ++ .../internal/integration/llm_service_test.go | 32 +++++++++++++++---- internal/ai/internal/service/jd_service.go | 18 ++++++++++- .../service/llm/handler/log/builder.go | 4 +-- internal/ai/internal/web/handler.go | 1 + internal/ai/internal/web/vo.go | 1 + 6 files changed, 49 insertions(+), 10 deletions(-) diff --git a/internal/ai/internal/domain/jd.go b/internal/ai/internal/domain/jd.go index b82bcf73..f77c4e0a 100644 --- a/internal/ai/internal/domain/jd.go +++ b/internal/ai/internal/domain/jd.go @@ -4,6 +4,7 @@ const ( AnalysisJDTech = "analysis_jd_tech" AnalysisJDBiz = "analysis_jd_biz" AnalysisJDPosition = "analysis_jd_position" + AnalysisJDSubtext = "analysis_jd_subtext" ) type JDEvaluation struct { @@ -16,4 +17,6 @@ type JD struct { TechScore *JDEvaluation BizScore *JDEvaluation PosScore *JDEvaluation + // 潜台词 + Subtext string } diff --git a/internal/ai/internal/integration/llm_service_test.go b/internal/ai/internal/integration/llm_service_test.go index d285f574..b7cb860c 100644 --- a/internal/ai/internal/integration/llm_service_test.go +++ b/internal/ai/internal/integration/llm_service_test.go @@ -59,6 +59,7 @@ func (s *LLMServiceSuite) SetupSuite() { err = s.db.Create(&dao.BizConfig{ Biz: domain.BizQuestionExamine, MaxInput: 100, + Price: 1, PromptTemplate: "这是问题 %s,这是问题内容 %s,这是用户输入 %s", KnowledgeId: knowledgeId, Ctime: now, @@ -68,6 +69,7 @@ func (s *LLMServiceSuite) SetupSuite() { err = s.db.Create(&dao.BizConfig{ Biz: domain.BizCaseExamine, MaxInput: 100, + Price: 1, PromptTemplate: "这是案例 %s,这是案例内容 %s,这是用户输入 %s", KnowledgeId: knowledgeId, Ctime: now, @@ -105,6 +107,15 @@ func (s *LLMServiceSuite) SetupSuite() { }).Error s.NoError(err) + err = s.db.Create(&dao.BizConfig{ + Biz: domain.AnalysisJDSubtext, + MaxInput: 100, + PromptTemplate: "这是岗位描述Subtext %s", + KnowledgeId: knowledgeId, + Ctime: now, + Utime: now, + }).Error + s.NoError(err) } func (s *LLMServiceSuite) TearDownSuite() { @@ -639,31 +650,37 @@ func (s *LLMServiceSuite) TestHandler_AnalysisJD() { llmHdl := hdlmocks.NewMockHandler(ctrl) llmHdl.EXPECT().Handle(gomock.Any(), gomock.Any()). DoAndReturn(func(ctx context.Context, request domain.LLMRequest) (domain.LLMResponse, error) { - if request.Biz == "analysis_jd_tech" { + switch request.Biz { + case domain.AnalysisJDTech: return domain.LLMResponse{ Tokens: 1000, Amount: 100, Answer: `score: 6 这是技术前景`, }, nil - } - if request.Biz == "analysis_jd_biz" { + case domain.AnalysisJDBiz: return domain.LLMResponse{ Tokens: 100, Amount: 200, Answer: `score: 7 这是业务前景`, }, nil - } - if request.Biz == "analysis_jd_position" { + case domain.AnalysisJDPosition: return domain.LLMResponse{ Tokens: 100, Amount: 100, Answer: `score: 8 这是公司地位`, }, nil + case domain.AnalysisJDSubtext: + return domain.LLMResponse{ + Tokens: 100, + Amount: 100, + Answer: `这是我的分析`, + }, nil + default: + return domain.LLMResponse{}, errors.New("unknown biz") } - return domain.LLMResponse{}, errors.New("unknown biz") }).AnyTimes() creditSvc := creditmocks.NewMockService(ctrl) creditSvc.EXPECT().GetCreditsByUID(gomock.Any(), gomock.Any()).Return(credit.Credit{ @@ -676,7 +693,7 @@ func (s *LLMServiceSuite) TestHandler_AnalysisJD() { after: func(t *testing.T, resp web.JDResponse) { // 校验response写入的内容是否正确 assert.Equal(t, web.JDResponse{ - Amount: 400, + Amount: 500, TechScore: &web.JDEvaluation{ Score: 6, Analysis: "这是技术前景", @@ -689,6 +706,7 @@ func (s *LLMServiceSuite) TestHandler_AnalysisJD() { Score: 8, Analysis: "这是公司地位", }, + Subtext: "这是我的分析", }, resp) }, diff --git a/internal/ai/internal/service/jd_service.go b/internal/ai/internal/service/jd_service.go index 093e947b..4d304088 100644 --- a/internal/ai/internal/service/jd_service.go +++ b/internal/ai/internal/service/jd_service.go @@ -3,6 +3,7 @@ package service import ( "context" "errors" + "fmt" "strconv" "strings" "sync/atomic" @@ -31,6 +32,7 @@ func NewJDService(aiSvc llm.Service) JDService { func (j *jdSvc) Evaluate(ctx context.Context, uid int64, jd string) (domain.JD, error) { var techJD, bizJD, positionJD *domain.JDEvaluation var amount int64 + var subtext string var eg errgroup.Group eg.Go(func() error { var err error @@ -62,6 +64,19 @@ func (j *jdSvc) Evaluate(ctx context.Context, uid int64, jd string) (domain.JD, atomic.AddInt64(&amount, positionAmount) return nil }) + + eg.Go(func() error { + tid := shortuuid.New() + resp, err := j.aiSvc.Invoke(ctx, domain.LLMRequest{ + Uid: uid, + Tid: tid, + Biz: domain.AnalysisJDSubtext, + Input: []string{jd}, + }) + subtext = resp.Answer + atomic.AddInt64(&amount, resp.Amount) + return err + }) if err := eg.Wait(); err != nil { return domain.JD{}, err } @@ -70,6 +85,7 @@ func (j *jdSvc) Evaluate(ctx context.Context, uid int64, jd string) (domain.JD, TechScore: techJD, BizScore: bizJD, PosScore: positionJD, + Subtext: subtext, }, nil } @@ -92,7 +108,7 @@ func (j *jdSvc) analysisJd(ctx context.Context, uid int64, biz string, jd string score := answer[0] scoreNum, err := strconv.ParseFloat(strings.TrimSpace(strings.TrimPrefix(score, "score:")), 64) if err != nil { - return 0, nil, errors.New("分数返回的数据不对") + return 0, nil, fmt.Errorf("分数返回的数据不对 %s", score) } return resp.Amount, &domain.JDEvaluation{ diff --git a/internal/ai/internal/service/llm/handler/log/builder.go b/internal/ai/internal/service/llm/handler/log/builder.go index c87629b7..df1cd81c 100644 --- a/internal/ai/internal/service/llm/handler/log/builder.go +++ b/internal/ai/internal/service/llm/handler/log/builder.go @@ -31,7 +31,7 @@ func (h *HandlerBuilder) Next(next handler.Handler) handler.Handler { elog.Int64("uid", req.Uid), elog.String("biz", req.Biz)) // 记录请求 - logger.Info("请求 LLM") + logger.Debug("请求 LLM") resp, err := next.Handle(ctx, req) if err != nil { // 记录错误 @@ -39,7 +39,7 @@ func (h *HandlerBuilder) Next(next handler.Handler) handler.Handler { return resp, err } // 记录响应 - logger.Info("请求 LLM 服务响应成功", elog.Int64("tokens", resp.Tokens)) + logger.Debug("请求 LLM 服务响应成功", elog.Int64("tokens", resp.Tokens)) return resp, err }) } diff --git a/internal/ai/internal/web/handler.go b/internal/ai/internal/web/handler.go index 40d0d403..dfdd55f3 100644 --- a/internal/ai/internal/web/handler.go +++ b/internal/ai/internal/web/handler.go @@ -65,6 +65,7 @@ func (h *Handler) AnalysisJd(ctx *ginx.Context, req JDRequest, sess session.Sess TechScore: h.newJD(resp.TechScore), BizScore: h.newJD(resp.BizScore), PosScore: h.newJD(resp.PosScore), + Subtext: resp.Subtext, }, }, nil default: diff --git a/internal/ai/internal/web/vo.go b/internal/ai/internal/web/vo.go index 4735c6b5..e47f07eb 100644 --- a/internal/ai/internal/web/vo.go +++ b/internal/ai/internal/web/vo.go @@ -19,6 +19,7 @@ type JDResponse struct { TechScore *JDEvaluation `json:"techScore"` BizScore *JDEvaluation `json:"bizScore"` PosScore *JDEvaluation `json:"posScore"` + Subtext string `json:"subtext"` } type JDEvaluation struct { From bc975ec3bc7c768048e81a8b5c65d863af158cfd Mon Sep 17 00:00:00 2001 From: Deng Ming Date: Thu, 5 Dec 2024 17:24:49 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E8=B0=83=E6=95=B4=E5=B2=97=E4=BD=8D?= =?UTF-8?q?=E5=88=86=E6=9E=90=E8=BF=94=E5=9B=9E=E6=95=B0=E6=8D=AE=E6=A0=BC?= =?UTF-8?q?=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/ai/internal/domain/jd.go | 6 +- .../internal/integration/llm_service_test.go | 21 +++---- internal/ai/internal/service/jd_service.go | 55 ++++++++++++------- .../ai/internal/service/jd_service_test.go | 50 +++++++++++++++++ internal/ai/internal/web/handler.go | 4 +- internal/ai/internal/web/vo.go | 10 ++-- ioc/private/nonsense/non_sense_v1.go | 4 +- 7 files changed, 108 insertions(+), 42 deletions(-) create mode 100644 internal/ai/internal/service/jd_service_test.go diff --git a/internal/ai/internal/domain/jd.go b/internal/ai/internal/domain/jd.go index f77c4e0a..ccebc8bd 100644 --- a/internal/ai/internal/domain/jd.go +++ b/internal/ai/internal/domain/jd.go @@ -14,9 +14,9 @@ type JDEvaluation struct { type JD struct { Amount int64 - TechScore *JDEvaluation - BizScore *JDEvaluation - PosScore *JDEvaluation + TechScore JDEvaluation + BizScore JDEvaluation + PosScore JDEvaluation // 潜台词 Subtext string } diff --git a/internal/ai/internal/integration/llm_service_test.go b/internal/ai/internal/integration/llm_service_test.go index b7cb860c..989fcb19 100644 --- a/internal/ai/internal/integration/llm_service_test.go +++ b/internal/ai/internal/integration/llm_service_test.go @@ -655,22 +655,19 @@ func (s *LLMServiceSuite) TestHandler_AnalysisJD() { return domain.LLMResponse{ Tokens: 1000, Amount: 100, - Answer: `score: 6 -这是技术前景`, + Answer: `{"score":6, "summary":["这是技术前景"]}`, }, nil case domain.AnalysisJDBiz: return domain.LLMResponse{ Tokens: 100, Amount: 200, - Answer: `score: 7 -这是业务前景`, + Answer: `{"score":7, "summary":["这是业务前景"]}`, }, nil case domain.AnalysisJDPosition: return domain.LLMResponse{ Tokens: 100, Amount: 100, - Answer: `score: 8 -这是公司地位`, + Answer: `{"score":8, "summary":["这是公司地位"]}`, }, nil case domain.AnalysisJDSubtext: return domain.LLMResponse{ @@ -694,17 +691,17 @@ func (s *LLMServiceSuite) TestHandler_AnalysisJD() { // 校验response写入的内容是否正确 assert.Equal(t, web.JDResponse{ Amount: 500, - TechScore: &web.JDEvaluation{ + TechScore: web.JDEvaluation{ Score: 6, - Analysis: "这是技术前景", + Analysis: "- 这是技术前景", }, - BizScore: &web.JDEvaluation{ + BizScore: web.JDEvaluation{ Score: 7, - Analysis: "这是业务前景", + Analysis: "- 这是业务前景", }, - PosScore: &web.JDEvaluation{ + PosScore: web.JDEvaluation{ Score: 8, - Analysis: "这是公司地位", + Analysis: "- 这是公司地位", }, Subtext: "这是我的分析", }, resp) diff --git a/internal/ai/internal/service/jd_service.go b/internal/ai/internal/service/jd_service.go index 4d304088..f9f810f6 100644 --- a/internal/ai/internal/service/jd_service.go +++ b/internal/ai/internal/service/jd_service.go @@ -2,35 +2,43 @@ package service import ( "context" - "errors" - "fmt" - "strconv" + "encoding/json" + "regexp" "strings" "sync/atomic" + "github.com/gotomicro/ego/core/elog" + "github.com/ecodeclub/webook/internal/ai/internal/domain" "github.com/ecodeclub/webook/internal/ai/internal/service/llm" "github.com/lithammer/shortuuid/v4" "golang.org/x/sync/errgroup" ) +// 最简单的提取方式 +const jsonExpr = `\{(.|\n|\r)+\}` + type JDService interface { // Evaluate 测评 Evaluate(ctx context.Context, uid int64, jd string) (domain.JD, error) } type jdSvc struct { - aiSvc llm.Service + aiSvc llm.Service + logger *elog.Component + expr *regexp.Regexp } func NewJDService(aiSvc llm.Service) JDService { return &jdSvc{ - aiSvc: aiSvc, + aiSvc: aiSvc, + logger: elog.DefaultLogger, + expr: regexp.MustCompile(jsonExpr), } } func (j *jdSvc) Evaluate(ctx context.Context, uid int64, jd string) (domain.JD, error) { - var techJD, bizJD, positionJD *domain.JDEvaluation + var techJD, bizJD, positionJD domain.JDEvaluation var amount int64 var subtext string var eg errgroup.Group @@ -89,7 +97,7 @@ func (j *jdSvc) Evaluate(ctx context.Context, uid int64, jd string) (domain.JD, }, nil } -func (j *jdSvc) analysisJd(ctx context.Context, uid int64, biz string, jd string) (int64, *domain.JDEvaluation, error) { +func (j *jdSvc) analysisJd(ctx context.Context, uid int64, biz string, jd string) (int64, domain.JDEvaluation, error) { tid := shortuuid.New() aiReq := domain.LLMRequest{ Uid: uid, @@ -99,20 +107,29 @@ func (j *jdSvc) analysisJd(ctx context.Context, uid int64, biz string, jd string } resp, err := j.aiSvc.Invoke(ctx, aiReq) if err != nil { - return 0, nil, err - } - answer := strings.SplitN(resp.Answer, "\n", 2) - if len(answer) != 2 { - return 0, nil, errors.New("不符合预期的大模型响应") + return 0, domain.JDEvaluation{}, err } - score := answer[0] - scoreNum, err := strconv.ParseFloat(strings.TrimSpace(strings.TrimPrefix(score, "score:")), 64) + jsonStr := j.expr.FindString(resp.Answer) + var ( + scoreResp ScoreResp + analysis string + ) + err = json.Unmarshal([]byte(jsonStr), &scoreResp) if err != nil { - return 0, nil, fmt.Errorf("分数返回的数据不对 %s", score) + j.logger.Error("不符合预期的大模型响应", + elog.FieldErr(err), + elog.String("resp", resp.Answer)) + } else { + analysis = "- " + strings.Join(scoreResp.Summary, "\n- ") } - - return resp.Amount, &domain.JDEvaluation{ - Score: scoreNum, - Analysis: strings.TrimSpace(strings.TrimPrefix(answer[1], "analysis:")), + return resp.Amount, domain.JDEvaluation{ + Score: scoreResp.Score, + // 按照 Markdown 的写法,拼接起来 + Analysis: analysis, }, nil } + +type ScoreResp struct { + Score float64 `json:"score"` + Summary []string `json:"summary"` +} diff --git a/internal/ai/internal/service/jd_service_test.go b/internal/ai/internal/service/jd_service_test.go new file mode 100644 index 00000000..468ad909 --- /dev/null +++ b/internal/ai/internal/service/jd_service_test.go @@ -0,0 +1,50 @@ +// Copyright 2023 ecodeclub +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package service + +import ( + "regexp" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TestJSONExpression 测试利用正则表达式提取 JSON 串 +func TestJSONExpression(t *testing.T) { + testCases := []struct { + name string + input string + want string + }{ + { + name: "本身就是JSON", + input: `{"abc": "bcd"}`, + want: `{"abc": "bcd"}`, + }, + { + name: "有前缀后缀", + input: "```json{\"abc\": \"bcd\"}```", + want: `{"abc": "bcd"}`, + }, + } + + expr := regexp.MustCompile(jsonExpr) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + val := expr.FindString(tc.input) + assert.Equal(t, tc.want, val) + }) + } +} diff --git a/internal/ai/internal/web/handler.go b/internal/ai/internal/web/handler.go index dfdd55f3..1e15e6c9 100644 --- a/internal/ai/internal/web/handler.go +++ b/internal/ai/internal/web/handler.go @@ -74,8 +74,8 @@ func (h *Handler) AnalysisJd(ctx *ginx.Context, req JDRequest, sess session.Sess } -func (h *Handler) newJD(jd *domain.JDEvaluation) *JDEvaluation { - return &JDEvaluation{ +func (h *Handler) newJD(jd domain.JDEvaluation) JDEvaluation { + return JDEvaluation{ Score: jd.Score, Analysis: jd.Analysis, } diff --git a/internal/ai/internal/web/vo.go b/internal/ai/internal/web/vo.go index e47f07eb..12b34622 100644 --- a/internal/ai/internal/web/vo.go +++ b/internal/ai/internal/web/vo.go @@ -15,11 +15,11 @@ type JDRequest struct { } type JDResponse struct { - Amount int64 `json:"amount"` - TechScore *JDEvaluation `json:"techScore"` - BizScore *JDEvaluation `json:"bizScore"` - PosScore *JDEvaluation `json:"posScore"` - Subtext string `json:"subtext"` + Amount int64 `json:"amount"` + TechScore JDEvaluation `json:"techScore"` + BizScore JDEvaluation `json:"bizScore"` + PosScore JDEvaluation `json:"posScore"` + Subtext string `json:"subtext"` } type JDEvaluation struct { diff --git a/ioc/private/nonsense/non_sense_v1.go b/ioc/private/nonsense/non_sense_v1.go index 7997745c..692b26cb 100644 --- a/ioc/private/nonsense/non_sense_v1.go +++ b/ioc/private/nonsense/non_sense_v1.go @@ -15,11 +15,13 @@ package nonsense import ( + "log/slog" + "github.com/gin-gonic/gin" ) // NonSenseV1 var NonSenseV1 gin.HandlerFunc = func(ct *gin.Context) { // 啥也不做 - println("hello") + slog.Debug("进来了 NonSenseV1") }