斗破Google ADK Java苍穹-第3境斗师-纱衣启灵-数字人Agent

斗师境:数字人 Agent 与 Tools——从写死提示词到项目级身份与边界

一个 Agent 服务 N 个项目,靠的不是提示词的自觉

[读者] 正在把数字人嵌进业务系统的 Java 后端
[痛点] 一个项目一套硬编码 Agent,改一句话就要重新发版
[现在读] 你已经跑通第一个 LlmAgent,第二个项目正在复制你的代码
[读完] 能交付“项目身份 + 人格 + 工具边界”全部来自配置的数字人
[旧方案] LlmAgent 写在 @Bean 里,systemInstruction 硬编码在常量类
    |
    v
[新需求] 一个项目一份人格,页面改完即生效,多项目共用一套运行代码
    |
    v
[冲突] Agent 是编译期对象,项目配置是运行期数据,中间没有桥
    |
    v
[后果] 每来一个项目复制一次代码;工具不认识用户,越权查资料没人拦

我是老李,在深圳带一队 Java 后端。去年我们做了一个「数字人客服」,其实就是一个 LlmAgent,提示词写在一个叫 Prompts 的常量类里。上线那天,大家挺高兴。

三个月后我们接了第二个客户。诉求很简单:语气更严肃一点、开场语换掉、资料必须隔离。我们的做法是新建一个 PromptsV2,复制一份 Agent 配置类,改完提交。第三个客户来的时候,我在 code review 里看到了 PromptsV3Final。

真正让我警觉的不是重复代码。是一次联调:测试同学在 A 客户的项目页面上问「帮我查一下这个项目的资料」,数字人张口就把 B 客户的课程大纲背了出来。因为工具是一个单例 Bean,它不知道此刻是哪个项目、是谁在问。

于是我们把这条线掰回来:一套后端,同时服务 N 个项目,每个项目有自己的身份、人格和工具边界,全部来自数据库配置,页面改完即生效。

数字人的人格,到底该存在哪里——代码、提示词,还是配置?

同一个 Agent 类,怎么同时服务 N 个项目而不串台?

工具怎么知道「你是谁、你在哪个项目里、你有没有权限」?

如果这三件事继续靠「写代码 + 复制 + 口头约定」解决,下一个客户到来时,我们交付的不是产品,而是一份需要专人维护的清单。

01、故事

周五下午三点,客户演示定在四点半。运营在页面上把开场语从「欢迎来到星火课堂」改成「同学你好,我是星火老师」,点保存,抬头问我:这样就能生效了吗?

我说不能,得等我发一版。

那一刻我知道问题不在运营,在我。我们的数字人是这样一个 Spring 单例:

@Configuration
public class LegacyDigitalHumanConfig {

  private static final String INSTRUCTION = """
      你是星火课堂的助教,说话亲切。
      开场语:欢迎来到星火课堂。
      """;

  @Bean
  public LlmAgent digitalHuman() {
    return LlmAgent.builder()
        .name("xinghuo_tutor")
        .model(Gemini.builder().modelName("gemini-2.5-flash").build())
        .instruction(INSTRUCTION)
        .tools(new ProjectTools())
        .build();
  }
}

这段代码里有三个致命细节。第一,人格是常量,项目是数据,两者永远对不上;第二,工具是无状态单例,它拿不到 projectId 和 userId;第三,工具内部直接查全表,没有任何归属校验。

任务是把「项目身份、人格、工具边界」从编译产物搬进数据库,并让同一个进程同时服务多个项目。运营改完点保存,下一个请求立刻生效。

变化是从「一个项目一套部署」变成「一套部署、N 个项目」。冲突也在这里:Agent 在 Spring 启动时就被 new 出来了,而项目配置是运行期写进库的。

演示之前改词
发版才能生效
单例不知是谁
张口就串了台

02、问题

旧方案失效得很彻底,而且是三处同时失效。

第一处,人格与项目耦合在编译产物里。instruction 是静态常量,ProjectConfig 是运行期数据,中间没有桥。

第二处,工具没有身份。new ProjectTools() 这个单例被所有项目共享,任何一次调用都拿不到「我是哪个项目、我为谁服务」。

第三处,没有 ACL。工具直接 select * from material,谁调都一样。

业务上的影响是三条。交付周期:每加一个项目等于一次发版加一次回归。数据安全:跨项目资料串台,这在多租户场景里是事故级问题。审计:事后无法回答「谁在什么时候、通过哪个数字人、读了哪个项目的什么字段」。

技术上的表现同样具体:出现 PromptsV3Final;工具类里开始出现 if ("p-1001".equals(...)) 这种分支;新增一个项目字段要改五个文件。

所以本境必须有一个可验证的完成标准,否则「配置化」只是口号:

  1. 新增一个项目只需要插入项目行与 Agent 配置行,不重新打包、不重新发版;
  2. 同一进程内,两个项目产出的 LlmAgent 的 name 与 instruction 不同,并且各自挂载同样的 3 个最小工具;
  3. 非项目成员触发工具时,返回结构化 status=DENIED,并落一条审计记录;
  4. 注册/登录、项目 CRUD、Agent 配置、网页运行这五类最小页面可用;
  5. mvn test 全绿,且包含工厂构造与 ACL 拒绝的用例。

人格长在代码
交付靠复制粘贴
工具不识来客
事故无从追溯

03、原理

LlmAgent 的构成其实不复杂:name、description、model、instruction、tools、subAgents。难的是知道每一件到底管什么。

name 是运行时标识。它出现在会话、日志、事件里,通常要求是一个合法标识符,不是给人看的漂亮标题。instruction 是行为契约,决定这个 Agent「怎么说、什么时候调用工具」。tools 是能力边界,决定它「能做什么」。description 是给别人看的——主要是给上层的编排者或父 Agent 看,用于「要不要把这件事委派给它」。

反直觉判断:description 基本不参与当前 Agent 的自我理解。很多人把性格、语气、规则一股脑塞进 description,然后困惑为什么模型不照着做。真正影响它行为的是 instruction,真正限制它行为的是 tools。description 是路由信息,不是人格信息。

也就是从这一境开始,我们区分两种字段:投影字段与人格字段。ProjectConfig 里的 name/title/themeColor/backgroundImage/openingText/closingText/status 是投影字段,决定这个数字人长什么样;AgentConfig 里的 agentName/model/systemInstruction 是人格字段,决定这个数字人是谁。

反直觉判断:想限制数字人乱说话,最有效的手段不是把提示词写得更长,而是缩小它的工具集合与参数空间。提示词是软约束,模型可以礼貌地无视它;工具 Schema 是硬约束,参数类型错了、字段名不在枚举里,调用根本发不出去。你写「不要查询其他项目的数据」一百遍,不如让工具只能拿到当前 projectId。

instruction 与 tools 是一对耦合件。工具有了,instruction 里必须明确「涉及项目资料时先调 getProjectProfile,不得凭记忆回答」,否则模型会用训练数据里的常识去补,看起来很像对的,实际是编的。反过来,instruction 也不用罗列,像 subAgents 这条线,本境先留出位置不启用,等能力真的需要编排时再挂上去。

最后一条代价必须先说清楚:配置化就是运行期构造。编译期的类型错误,会变成运行期的构造失败。所以校验必须前移——保存时校验一次,构造时再校验一次,任何一处不合法都要能被立刻发现,而不是等用户点开运行页才炸。

人格不在词里
能力不在嘴上
工具划出边界
身份写进参数

04、架构

[输入] {projectId + userId + message}
    |
    v
[模块] {ProjectConfig / AgentConfig / DigitalHumanAgentFactory}
    |
    v
[数据/状态] {项目七字段 + Agent 三字段 + 成员关系}
    |
    v
[处理] {运行期构造 LlmAgent -> 工具带身份注入 -> 工具内 ACL 判定}
    |
    v
[输出] {文本回复 + 结构化工具结果 + 审计记录}

这条链上只有两个真正重要的决定。

第一,Agent 不再是 Bean,而是工厂的产物。工厂的入口是 projectId 与 userId,出口是一个配置完整、工具已绑定的 LlmAgent。它放在哪一层由业务决定,但一定不是 Spring 容器启动时被创建的那个单例。

第二,工具不是无状态单例,而是带着身份被 new 出来的对象。projectId 和 userId 通过构造参数注入,工具内部用它们做归属校验,返回结构化结果。工具的生命周期跟一次请求绑定,不跟应用绑定。

边界也要说清楚。这个架构解决的是「一个进程服务 N 个项目、每项目独立人格与独立数据边界」;它不解决长会话记忆、不解决多步流程编排、不解决远程工具调用,那些属于后面的境界。

收益有两条:一是加项目变成写数据,交付成本从「发版」降到「配置」;二是越权从「靠自觉」变成「靠代码」,因为工具在参数层面就只能是当前项目。

代价同样有两条:一是每次请求都要构造 LlmAgent,有构造开销,需要缓存「人格」而不是缓存「整个 Agent」;二是配置错误从编译期推迟到运行期,必须补上保存校验与启动自检。

适用条件是:项目之间的人格差异可以用「指令 + 项目资料」表达。如果两个项目需要完全不同的推理流程与工具链,那就不是同一类数字人,应该拆成不同的 Agent 定义,而不是硬塞进一个 instruction。

配置沉到库里
工厂产 Agent
工具挟身份走
结果留审计痕

05、实战一次

环境与版本

JDK 21、Maven 3.9+、ADK Java 1.11.0、Spring Boot 3.x(具体小版本以官方当前稳定版为准)、H2 内存库。分支 chapter/03-digital-human。

依赖

<properties>
  <java.version>21</java.version>
  <adk.version>1.11.0</adk.version>
</properties>

<dependencies>
  <dependency>
    <groupId>com.google.adk</groupId>
    <artifactId>google-adk</artifactId>
    <version>${adk.version}</version>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
  </dependency>
  <dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
  </dependency>
  <dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
  </dependency>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
  </dependency>
</dependencies>

配置

spring:
  datasource:
    url: jdbc:h2:mem:dh03;DB_CLOSE_DELAY=-1
    username: sa
    password: ""
  jpa:
    hibernate:
      ddl-auto: update

app:
  digital-human:
    max-instruction-length: 8000
    default-zone: Asia/Shanghai

模型凭证走环境变量(官方文档中 Gemini 侧的变量名以当前版本为准):

export GOOGLE_API_KEY=your-api-key
export GOOGLE_GENAI_USE_VERTEXAI=false

核心实现

项目七字段与 Agent 三字段,严格按约定落表。

package com.example.dh.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.Instant;

@Entity
@Table(name = "project")
@Getter
@Setter
@NoArgsConstructor
public class ProjectConfig {

  @Id
  private String id;

  @Column(nullable = false)
  private String ownerId;

  @Column(nullable = false, unique = true)
  private String name;

  @Column(nullable = false)
  private String title;

  private String themeColor;

  private String backgroundImage;

  @Column(length = 2000)
  private String openingText;

  @Column(length = 2000)
  private String closingText;

  @Column(nullable = false)
  private String status;

  private Instant updatedAt;

  public boolean published() {
    return "PUBLISHED".equals(status);
  }
}
package com.example.dh.domain;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import java.time.Instant;

@Entity
@Table(name = "agent_config")
@Getter
@Setter
@NoArgsConstructor
public class AgentConfig {

  @Id
  private String id;

  @Column(nullable = false, unique = true)
  private String projectId;

  @Column(nullable = false)
  private String agentName;

  @Column(nullable = false)
  private String model;

  @Column(nullable = false, length = 8000)
  private String systemInstruction;

  private Instant updatedAt;
}

成员关系表决定 ACL 的事实来源:

package com.example.dh.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.UniqueConstraint;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Entity
@Table(name = "project_member",
    uniqueConstraints = @UniqueConstraint(columnNames = {"projectId", "userId"}))
@Getter
@Setter
@NoArgsConstructor
public class ProjectMember {

  @Id
  private String id;

  private String projectId;

  private String userId;

  private String role;
}

三个最小 Function Tools。注意每个方法都返回 Map,形成稳定的结构化返回;参数上用 @Schema 描述,方法本身通过 FunctionTool.create 反射绑定:

package com.example.dh.agent;

import com.example.dh.domain.AuditLog;
import com.example.dh.domain.ProjectConfig;
import com.example.dh.domain.User;
import com.example.dh.repo.AuditLogRepository;
import com.example.dh.repo.ProjectConfigRepository;
import com.example.dh.repo.ProjectMemberRepository;
import com.example.dh.repo.UserRepository;
import com.google.adk.tools.Annotations.Schema;

import java.time.DateTimeException;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Map;

public class DigitalHumanTools {

  private static final String OK = "OK";
  private static final String DENIED = "DENIED";

  private final String projectId;
  private final String userId;
  private final ProjectMemberRepository memberRepo;
  private final ProjectConfigRepository projectRepo;
  private final UserRepository userRepo;
  private final AuditLogRepository auditRepo;

  public DigitalHumanTools(String projectId,
                           String userId,
                           ProjectMemberRepository memberRepo,
                           ProjectConfigRepository projectRepo,
                           UserRepository userRepo,
                           AuditLogRepository auditRepo) {
    this.projectId = projectId;
    this.userId = userId;
    this.memberRepo = memberRepo;
    this.projectRepo = projectRepo;
    this.userRepo = userRepo;
    this.auditRepo = auditRepo;
  }

  @Schema(name = "field", description = "要查询的项目字段,取值:title / themeColor / openingText / closingText")
  public Map<String, Object> getProjectProfile(String field) {
    if (!memberRepo.existsByProjectIdAndUserId(projectId, userId)) {
      auditRepo.save(new AuditLog(projectId, userId, "getProjectProfile", DENIED));
      return Map.of("status", DENIED, "message", "当前用户不属于该项目,无法读取项目资料");
    }
    ProjectConfig p = projectRepo.findById(projectId)
        .orElseThrow(() -> new IllegalStateException("项目不存在: " + projectId));

    String value = switch (field == null ? "" : field) {
      case "title" -> p.getTitle();
      case "themeColor" -> p.getThemeColor();
      case "openingText" -> p.getOpeningText();
      case "closingText" -> p.getClosingText();
      default -> null;
    };
    if (value == null) {
      return Map.of("status", "INVALID_ARGUMENT", "message", "不支持的字段: " + field);
    }
    auditRepo.save(new AuditLog(projectId, userId, "getProjectProfile", OK));
    return Map.of("status", OK, "projectId", projectId, "field", field, "value", value);
  }

  @Schema(name = "getUserProfile", description = "查询当前对话用户的公开信息")
  public Map<String, Object> getUserProfile() {
    if (userId == null || userId.isBlank()) {
      return Map.of("status", DENIED, "message", "缺少用户身份");
    }
    User u = userRepo.findById(userId).orElse(null);
    if (u == null) {
      return Map.of("status", "NOT_FOUND", "message", "用户不存在: " + userId);
    }
    return Map.of("status", OK, "userId", u.getId(), "displayName", u.getDisplayName());
  }

  @Schema(name = "zoneId", description = "IANA 时区,例如 Asia/Shanghai")
  public Map<String, Object> getCurrentTime(String zoneId) {
    ZoneId zone;
    try {
      zone = ZoneId.of(zoneId == null || zoneId.isBlank() ? "Asia/Shanghai" : zoneId);
    } catch (DateTimeException e) {
      return Map.of("status", "INVALID_ARGUMENT", "message", "非法时区: " + zoneId);
    }
    return Map.of("status", OK, "zoneId", zone.getId(),
        "time", ZonedDateTime.now(zone).toString());
  }
}

工厂是本境的主角。它在运行期把数据库里的两行配置,翻译成一个可执行的 Agent:

package com.example.dh.agent;

import com.example.dh.domain.AgentConfig;
import com.example.dh.domain.ProjectConfig;
import com.example.dh.repo.AgentConfigRepository;
import com.example.dh.repo.AuditLogRepository;
import com.example.dh.repo.ProjectConfigRepository;
import com.example.dh.repo.ProjectMemberRepository;
import com.example.dh.repo.UserRepository;
import com.google.adk.agents.LlmAgent;
import com.google.adk.models.Gemini;
import com.google.adk.tools.FunctionTool;
import org.springframework.stereotype.Component;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;

@Component
public class DigitalHumanAgentFactory {

  private static final Pattern AGENT_NAME = Pattern.compile("[A-Za-z_][A-Za-z0-9_]{0,63}");

  private final ProjectConfigRepository projectRepo;
  private final AgentConfigRepository agentRepo;
  private final ProjectMemberRepository memberRepo;
  private final UserRepository userRepo;
  private final AuditLogRepository auditRepo;

  private final Map<String, String> instructionCache = new ConcurrentHashMap<>();

  public DigitalHumanAgentFactory(ProjectConfigRepository projectRepo,
                                  AgentConfigRepository agentRepo,
                                  ProjectMemberRepository memberRepo,
                                  UserRepository userRepo,
                                  AuditLogRepository auditRepo) {
    this.projectRepo = projectRepo;
    this.agentRepo = agentRepo;
    this.memberRepo = memberRepo;
    this.userRepo = userRepo;
    this.auditRepo = auditRepo;
  }

  public LlmAgent create(String projectId, String userId) {
    AgentConfig cfg = agentRepo.findByProjectId(projectId)
        .orElseThrow(() -> new DigitalHumanConfigException("项目未配置数字人: " + projectId));
    ProjectConfig project = projectRepo.findById(projectId)
        .orElseThrow(() -> new DigitalHumanConfigException("项目不存在: " + projectId));

    if (!project.published()) {
      throw new DigitalHumanConfigException("项目未发布: " + project.getStatus());
    }
    if (!memberRepo.existsByProjectIdAndUserId(projectId, userId)) {
      throw new DigitalHumanConfigException("用户不属于该项目: " + userId);
    }
    if (!AGENT_NAME.matcher(cfg.getAgentName()).matches()) {
      throw new DigitalHumanConfigException("agentName 非法: " + cfg.getAgentName());
    }

    String cacheKey = projectId + "@" + cfg.getUpdatedAt() + "@" + project.getUpdatedAt();
    String instruction = instructionCache.computeIfAbsent(cacheKey,
        key -> renderInstruction(project, cfg));

    DigitalHumanTools tools = new DigitalHumanTools(
        projectId, userId, memberRepo, projectRepo, userRepo, auditRepo);

    return LlmAgent.builder()
        .name(cfg.getAgentName())
        .description(project.getTitle() + " 专属数字人")
        .model(Gemini.builder().modelName(cfg.getModel()).build())
        .instruction(instruction)
        .tools(
            FunctionTool.create(tools, "getProjectProfile"),
            FunctionTool.create(tools, "getUserProfile"),
            FunctionTool.create(tools, "getCurrentTime"))
        .build();
  }

  private String renderInstruction(ProjectConfig project, AgentConfig cfg) {
    return """
        你是「%s」项目专属的数字人助手,只服务这一个项目。

        【项目人格】
        %s

        【项目信息】
        开场语:%s
        结束语:%s

        【行为契约】
        1. 需要项目资料时,只能通过 getProjectProfile 获取,不得凭记忆或推测回答。
        2. 需要用户信息时,调用 getUserProfile。
        3. 涉及“现在几点、今天几号”时,调用 getCurrentTime。
        4. 工具返回 status=DENIED 时,如实告知用户无权限,禁止绕过。
        5. 工具返回 status=INVALID_ARGUMENT 时,修正参数后重试一次。
        """.formatted(
        project.getTitle(),
        cfg.getSystemInstruction(),
        nullToEmpty(project.getOpeningText()),
        nullToEmpty(project.getClosingText()));
  }

  private String nullToEmpty(String s) {
    return s == null ? "" : s;
  }
}

文本模式的运行入口,本境先用最小 Runner 验证闭环,正式的 Runner 与 Event 体系在下一境展开:

package com.example.dh.web;

import com.example.dh.agent.DigitalHumanAgentFactory;
import com.google.adk.agents.LlmAgent;
import com.google.adk.events.Event;
import com.google.adk.runners.InMemoryRunner;
import com.google.adk.sessions.Session;
import com.google.genai.types.Content;
import com.google.genai.types.Part;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api")
public class DigitalHumanController {

  private final DigitalHumanAgentFactory factory;

  public DigitalHumanController(DigitalHumanAgentFactory factory) {
    this.factory = factory;
  }

  @PostMapping("/projects/{projectId}/chat")
  public ChatResponse chat(@PathVariable String projectId,
                           @RequestBody ChatRequest request,
                           @RequestHeader("X-User-Id") String userId) {

    LlmAgent agent = factory.create(projectId, userId);
    InMemoryRunner runner = new InMemoryRunner(agent);
    Session session = runner.sessionService()
        .createSession(agent.name(), userId)
        .blockingGet();

    StringBuilder reply = new StringBuilder();
    runner.runAsync(userId, session.id(),
            Content.fromParts(Part.fromText(request.message())))
        .blockingForEach(event -> appendText(event, reply));

    return new ChatResponse(session.id(), reply.toString());
  }

  private void appendText(Event event, StringBuilder reply) {
    event.content().ifPresent(content ->
        content.parts().ifPresent(parts ->
            parts.forEach(part -> part.text().ifPresent(reply::append))));
  }

  public record ChatRequest(String message) {}

  public record ChatResponse(String sessionId, String reply) {}
}

五类最小页面与它们对应的接口:

页面 路由 说明
注册/登录 /register、/login POST /api/auth/register、/api/auth/login
项目列表 /projects GET/POST/DELETE /api/projects
项目编辑 /projects/{id}/edit PUT /api/projects/{id},维护七字段
Agent 配置 /projects/{id}/agent GET/PUT /api/projects/{id}/agent,维护三字段
运行页 /projects/{id}/run POST /api/projects/{id}/chat,文本模式

运行页只保留一个输入框、一个气泡区和一串工具调用提示:

<section class="runner" data-project-id="p-1001">
  <div id="bubbles" class="bubbles"></div>
  <textarea id="message" rows="3" placeholder="问点什么……"></textarea>
  <button id="send">发送</button>
</section>

<script>
  const projectId = document.querySelector('.runner').dataset.projectId;

  document.getElementById('send').addEventListener('click', async () => {
    const input = document.getElementById('message');
    const text = input.value.trim();
    if (!text) return;
    appendBubble('我', text);
    input.value = '';

    const res = await fetch(`/api/projects/${projectId}/chat`, {
      method: 'POST',
      headers: {'Content-Type': 'application/json', 'X-User-Id': 'u-9'},
      body: JSON.stringify({message: text})
    });
    const data = await res.json();
    appendBubble('数字人', data.reply);
  });

  function appendBubble(who, text) {
    const el = document.createElement('div');
    el.className = 'bubble';
    el.textContent = `${who}:${text}`;
    document.getElementById('bubbles').appendChild(el);
  }
</script>

启动

mvn -q clean package
java -jar target/digital-human-03.jar

预期日志中应能看到应用启动、H2 建表与端口监听:

Tomcat started on port 8080 (http)
Started DigitalHumanApplication in 4.1 seconds

请求

curl -s -X POST http://localhost:8080/api/projects/p-1001/chat \
  -H "Content-Type: application/json" \
  -H "X-User-Id: u-9" \
  -d '{"message":"这个项目的开场语是什么?"}'

示例输出:

{
  "sessionId": "sess-7f2a",
  "reply": "这个项目的开场语是:同学你好,我是星火老师。"
}

验证

未在当前环境实测,以下为预期结果的测试用例:

package com.example.dh.agent;

import com.google.adk.agents.LlmAgent;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.A
posted @ 2026-10-10 22:24  李福春  阅读(2)  评论(0)    收藏  举报