package com.xly.config; import com.fasterxml.jackson.databind.ObjectMapper; import com.xly.agent.AgentIdentity; import com.xly.agent.ReActAgent; import com.xly.service.AuditService; import com.xly.service.ErpClient; import com.xly.service.FormResolverService; import com.xly.service.OpService; import com.xly.service.SkillService; import com.xly.service.SystemPromptService; import com.xly.tool.ErpReadTool; import com.xly.tool.FormCollectTool; import com.xly.tool.InteractionTool; import com.xly.tool.KgQueryTool; import com.xly.tool.ProposeWriteTool; import com.xly.tool.QueryTool; import com.xly.tool.SkillTool; import dev.langchain4j.memory.chat.MessageWindowChatMemory; import dev.langchain4j.model.ollama.OllamaChatModel; import dev.langchain4j.model.ollama.OllamaStreamingChatModel; import dev.langchain4j.service.AiServices; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Component; /** * 按「每次请求的身份」组装单一 ReAct agent(架构 §5/§7 的 per-call context)。 * *

为什么按请求新建:LangChain4j 流式工具执行发生在模型 HTTP 回调线程,而非控制器工作线程, * ThreadLocal 不可靠。于是把 {@link AgentIdentity}(透传 token + 权限集)直接注入到**每请求新建**的 * 工具实例里(ErpReadTool/ProposeWriteTool/QueryTool/FormCollectTool),从根上保证鉴权与 token 正确。 * *

无状态、全局的工具({@link KgQueryTool} 表单目录/KG、{@link SkillTool} Skill 加载、 * {@link InteractionTool} AskUser)是单例、跨请求复用。模型、记忆存储、system prompt 也复用。 */ @Component public class AgentFactory { private final OllamaStreamingChatModel streamingModel; private final OllamaChatModel sqlModel; private final RedisChatMemoryStore memoryStore; private final SystemPromptService systemPromptService; private final ErpClient erp; private final JdbcTemplate jdbc; private final FormResolverService resolver; private final OpService ops; private final ObjectMapper mapper; private final AuditService audit; private final SkillService skillService; private final KgQueryTool kgQueryTool; private final SkillTool skillTool; private final InteractionTool interactionTool; private volatile String cachedSystemPrompt; public AgentFactory(@Qualifier("agentStreamingModel") OllamaStreamingChatModel streamingModel, @Qualifier("sqlChatModel") OllamaChatModel sqlModel, RedisChatMemoryStore memoryStore, SystemPromptService systemPromptService, ErpClient erp, JdbcTemplate jdbc, FormResolverService resolver, OpService ops, ObjectMapper mapper, AuditService audit, SkillService skillService, KgQueryTool kgQueryTool, SkillTool skillTool, InteractionTool interactionTool) { this.streamingModel = streamingModel; this.sqlModel = sqlModel; this.memoryStore = memoryStore; this.systemPromptService = systemPromptService; this.erp = erp; this.jdbc = jdbc; this.resolver = resolver; this.ops = ops; this.mapper = mapper; this.audit = audit; this.skillService = skillService; this.kgQueryTool = kgQueryTool; this.skillTool = skillTool; this.interactionTool = interactionTool; } /** system prompt 是全局的(L1 域图 + Skill 摘要),构建一次后缓存。 */ private String systemPrompt() { String p = cachedSystemPrompt; if (p == null) { synchronized (this) { if (cachedSystemPrompt == null) { cachedSystemPrompt = systemPromptService.buildSystemPrompt(); } p = cachedSystemPrompt; } } return p; } /** 用给定身份组装一个 ReAct agent(工具实例携带该身份的 token 与权限边界)。 */ public ReActAgent build(AgentIdentity identity) { String systemPrompt = systemPrompt(); return AiServices.builder(ReActAgent.class) .streamingChatModel(streamingModel) .tools( kgQueryTool, skillTool, interactionTool, new ErpReadTool(erp, jdbc, resolver, identity), new ProposeWriteTool(erp, jdbc, ops, mapper, identity, resolver), new QueryTool(sqlModel, jdbc, audit, identity), new FormCollectTool(erp, jdbc, resolver, identity, mapper)) .chatMemoryProvider(memoryId -> MessageWindowChatMemory.builder() .id(memoryId) .maxMessages(30) .chatMemoryStore(memoryStore) .build()) .systemMessageProvider(memoryId -> systemPrompt) .build(); } }