1. 审批到一半想拉人商量,BPMN 里没画这个节点怎么办
先说场景。政务 OA 里最常见的一幕:处长审到一半,觉得这个事项得问问纪检科的意见,于是想临时拉两个人进来一起看看。问题来了——Activiti 的multiInstance多实例会签,参与者必须在 BPMN 定义时就写死,流程部署之后改不了。你不可能为了"临时拉人"去重新画流程图、重新部署、再让在途流程全部迁移。
这就是"随时会签"要解决的核心矛盾:会签是运行时动作,不是设计时约束。BPMN 定义的是骨架(先科长、再处长、再局长),但"处长到局长之间要不要拉人商量",这个决定应该由审批人当场做。
我试过的方案是不碰multiInstance,改用"外挂子流程 + 关联表":主流程走到任意节点,审批人点一个"会签"按钮,系统启动一个独立的 Activiti 流程实例(key=hq),在关联表T_HJS_s里记录父子流程关系,会签发起人当场从用户树上勾选参与者,每个参与者独立审批,主流程通过gethqinfo()轮询会签状态,全部签完才放行。
这套方案的关键词是:Activiti、Java、工作流、会签、BPMN。它适合谁?适合正在用 Activiti 做政务/企业审批、被"动态加签"需求卡住的 Java 后端。下面我把 BPMN 会签节点配置、动态加签监听器代码、以及用 TaoToken 统一 Key 做多模型审批意见摘要的完整链路拆开讲,每一步都能复制。
先明确一个边界:本文不教你绕过任何合规流程,只讲技术实现。会签参与者是谁、审批规则怎么定,由你的业务和制度决定,代码只负责把"临时拉人"这件事在运行时跑通。
2. TaoToken 统一 Key 前置:给会签意见摘要接一个多模型通道
会签场景有个隐藏痛点:五个人签完,意见五花八门,主流程审批人(比如局长)要一条条看。如果能自动生成一段"会签意见摘要",把通过/不通过/关键分歧点提炼出来,审批效率会高很多。而摘要这种任务,不同模型各有擅长——有的擅长长文本归纳,有的擅长抓分歧点。如果每个模型都单独申请 Key、单独配 Base URL,代码里会散落一堆配置。
TaoToken 在这里的作用是统一 Key + 统一 API 通道:一个 Key、一个 Base URL,就能在代码里切换不同模型,不用为每个模型维护一套鉴权。对会签摘要这种"可能换模型试效果"的场景很合适。
你需要先拿到两样东西:
- API Key:在控制台创建,地址是
https://taotoken.net/api-keys(deep link 带归因:?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite) - Base URL:
https://taotoken.net/api(注意,API 地址不加 UTM 参数)
模型对话调试入口在https://taotoken.net/models(带utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite),你可以先在网页上把摘要的 prompt 调好,再搬进 Java 代码。接入文档在https://taotoken.net/doc(带utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite)。
这里要强调一个原则:TaoToken 是模型调用通道,不是工作流引擎的替代品。Activiti 负责流程流转,TaoToken 负责在流程节点里调用模型生成摘要,两者职责分离。会签的加签、减签、状态判断,全部由 Activiti + 关联表完成,模型只做文本处理。
如果你后续要做长期的编码或 Agent 类任务,可以了解 Coding Plan(https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_content=coding-plan&utm_campaign=rewrite);但本文的会签摘要属于轻量调用,用普通 API Key 即可。
配置上,我建议把模型调用参数抽成一个独立的配置文件,别硬编码在业务类里。下一节给出可复制的配置片段。
3. 可复制配置:BPMN 会签节点、关联表与模型调用参数
这一节是全文的技术核心,分三块:BPMN 会签子流程定义、T_HJS_s关联表结构、以及模型调用的 JSON 配置。
3.1 BPMN 会签子流程(key=hq)
会签子流程本身是一个极简的独立流程定义,只有一个用户任务节点,参与者由发起人运行时指定。hq.bpmn20.xml关键片段:
<process id="hq" name="会签子流程" isExecutable="true"> <startEvent id="hqStart"/> <userTask id="hqTask" name="会签审批" activiti:assignee="${assignee}" activiti:candidateUsers="${candidateUsers}"/> <endEvent id="hqEnd"/> <sequenceFlow id="f1" sourceRef="hqStart" targetRef="hqTask"/> <sequenceFlow id="f2" sourceRef="hqTask" targetRef="hqEnd"/> </process>注意activiti:assignee="${assignee}"用的是流程变量,启动实例时传入。这样同一个hq流程定义可以被无数次会签复用,参与者完全动态。
3.2 关联表 T_HJS_s 结构
父子流程关系靠这张表维护,字段和原文保持一致:
CREATE TABLE T_HJS_s ( proc_inst_id_ VARCHAR(64) NOT NULL COMMENT '会签子流程实例ID', pre_proc_inst_id_ VARCHAR(64) NOT NULL COMMENT '主流程实例ID', matter_name VARCHAR(255) COMMENT '会签事项名称', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (proc_inst_id_), KEY idx_pre (pre_proc_inst_id_) );审批结果表T_HJ_s记录每个参与者的意见:
CREATE TABLE T_HJ_s ( proc_inst_id_ VARCHAR(64) NOT NULL COMMENT '子流程实例ID', pre_proc_inst_id_ VARCHAR(64) NOT NULL COMMENT '主流程实例ID', user_id VARCHAR(64) COMMENT '参与者', dept_id VARCHAR(64) COMMENT '部门', pass CHAR(1) COMMENT '1通过 0不通过', cause VARCHAR(1000) COMMENT '意见', enddate DATETIME COMMENT '签批时间', PRIMARY KEY (proc_inst_id_, user_id) );3.3 模型调用配置(settings 风格 JSON)
把 TaoToken 的调用参数抽成taotoken.json,路径放在src/main/resources/config/taotoken.json:
{ "baseUrl": "https://taotoken.net/api", "apiKey": "${TAOTOKEN_API_KEY}", "model": "claude-sonnet-4-20250514", "timeoutMs": 30000, "summaryPrompt": "你是政务审批助手。以下是会签参与者的审批意见,请提炼:1) 通过人数与不通过人数;2) 主要分歧点;3) 一句话结论。意见列表:\n{opinions}" }apiKey用环境变量注入,别写死在文件里。model字段就是统一 Key 的价值所在——换模型只改这一行,Base URL 和鉴权方式不变。如果你用 Cline MCP 或 Codex 的auth.json做本地调试,三件套要写全:Base URL 填https://taotoken.net/api,Key 填你的 API Key,Model ID 填上面model的值,缺一个都会报鉴权或模型不存在。
3.4 动态加签监听器代码
会签发起人提交参与者列表后,后端为每个参与者启动一个子流程任务。核心是completeS():
public DataCenter completeS(DataCenter center, HttpServletRequest request) throws Exception { String preinsid = request.getParameter("preinsid"); String[] userIds = request.getParameterValues("userIds"); DBUtil.BeginTrans(null, false); for (String uid : userIds) { HashMap<String, String> vars = new HashMap<>(); vars.put("assignee", uid); vars.put("candidateUsers", uid); HashMap<String, String> task = processService.start("hq", uid, vars); DataStore ds = new DataStore("ds"); ds.setParameter("ins", "T_HJS_s"); Row row = ds.getRowset().add(); row.setItemValue("proc_inst_id_", task.get("insid")); row.setItemValue("pre_proc_inst_id_", preinsid); row.setItemValue("matter_name", "会签"); row.set_t(1); sqlcl.CommonSaveofbyseach(ds, null, null); } DBUtil.EndTrans(); return new DataCenter(); }这段代码就是"动态加签"的落点:userIds是发起人当场勾选的,循环里为每个人启动一个独立子流程实例,并在关联表登记。想加人?再调一次即可。想减人?删掉对应T_HJS_s记录并终止子流程实例。
4. 验证请求:从发起会签到模型摘要端到端跑通
配置写完,得验证。我按"发起会签 → 参与者签批 → 状态查询 → 模型摘要"四步走。
4.1 发起会签请求
前端hq()按钮触发后,后端starthq()启动主会签流程并跳转发起人界面。用 curl 模拟发起(实际项目里是表单提交):
curl -X POST "http://localhost:8080/BusinessAction?Business=hq&Action=starthq" \ -d "key=hq" \ -d "userid=zhangsan" \ -d "preinsid=25001" \ -d "matter_name=某社保事项"预期返回:T_HJS_s新增一行,proc_inst_id_是新的子流程实例 ID,pre_proc_inst_id_是25001。
4.2 参与者签批
参与者打开待办,看到主流程表单(只读),选通过/不通过并填意见,提交到completeP():
curl -X POST "http://localhost:8080/BusinessAction?Business=hq&Action=completeP" \ -d "insid=25002" \ -d "preinsid=25001" \ -d "pass=1" \ -d "cause=同意,建议补充材料"预期:T_HJ_s新增一行,子流程推进一步。所有参与者签完后,currTask(insid)返回 null,触发站内消息通知发起人。
4.3 状态查询 gethqinfo
主流程前端调gethqinfo()判断能否继续:
curl "http://localhost:8080/BusinessAction?Business=flow&Action=gethqinfo&insid=25001"返回code=0表示会签全部完成,提交按钮恢复;code=-1表示还有人没签,按钮保持禁用。四种状态判断:未签批(enddate为空)、放弃(procInstId为 null)、通过(pass=1)、不通过(pass=0)。
4.4 模型生成会签意见摘要
会签完成后,把T_HJ_s里的意见拼成列表,调 TaoToken 生成摘要。Java 侧用 OkHttp:
public String summarize(List<HjRecord> records) throws IOException { String opinions = records.stream() .map(r -> r.getUserId() + ":" + (r.getPass().equals("1") ? "通过" : "不通过") + ",意见:" + r.getCause()) .collect(Collectors.joining("\n")); String prompt = config.getSummaryPrompt().replace("{opinions}", opinions); JSONObject body = new JSONObject(); body.put("model", config.getModel()); body.put("messages", new JSONArray().put( new JSONObject().put("role", "user").put("content", prompt))); Request req = new Request.Builder() .url(config.getBaseUrl() + "/v1/chat/completions") .header("Authorization", "Bearer " + config.getApiKey()) .post(RequestBody.create(body.toString(), MediaType.parse("application/json"))) .build(); try (Response resp = client.newCall(req).execute()) { JSONObject json = new JSONObject(resp.body().string()); return json.getJSONArray("choices").getJSONObject(0) .getJSONObject("message").getString("content"); } }成功结果长这样:
通过 3 人,不通过 1 人。主要分歧:材料完整性。结论:建议补充材料后重新会签。把这段摘要回填到主流程表单的"会签意见"区域,局长审批时一眼就能看到重点。整个链路:Activiti 负责流转,TaoToken 负责摘要,职责清晰。
5. 本篇常见错排查:401、local proxy failed 与 reading choices
跑这套链路,我踩过的坑集中在模型调用和流程状态两块。逐个对照。
报错一:401 Unauthorized。最常见。原因通常是 Key 没注入或 Base URL 写错。检查taotoken.json里baseUrl是不是https://taotoken.net/api(不要带 UTM,不要带/v1之外的路径),apiKey环境变量TAOTOKEN_API_KEY是否真的导出。如果你用 Cline MCP 或 Codexauth.json调试,三件套必须齐全:Base URL、Key、Model ID,缺 Model ID 会报模型不存在,缺 Key 直接 401。
报错二:local proxy failed。这个报错一般出现在本地网络层,不是 TaoToken 返回的。检查你的 HTTP 客户端有没有误配系统代理,或者timeoutMs设得太短导致连接被掐断。把超时调到 30000ms 以上,并确认baseUrl是直连地址。
报错三:reading 'choices' of undefined。说明响应体里没有choices字段,通常是请求体格式不对——比如messages没包成数组,或者model字段为空。打印原始响应体再解析,别直接.getJSONArray("choices")。另外,如果模型返回的是流式(stream),要按 SSE 解析,不能当普通 JSON 读。
报错四:OAuth 相关错误。如果你在 Claude Code 或类似工具里配置,注意区分 API Key 鉴权和 OAuth 鉴权。TaoToken 走的是 Bearer API Key,不是 OAuth 流程。看到 OAuth 报错,先确认你填的是 API Key 而不是别的凭证。
流程侧报错:会签状态一直 code=-1。检查T_HJ_s里是否有pass=0的记录——有人不通过时canNext会一直是 false,这是设计如此。另外确认gethqinfo()查的是pre_proc_inst_id_而不是子流程 ID,查错字段会永远查不到记录。
流程侧报错:子流程实例启动后没有任务。检查hq.bpmn20.xml里activiti:assignee的变量名和processService.start()传入的varskey 是否一致。变量名对不上,任务会创建但 assignee 为空,待办列表里看不到。
排障时建议先单独验证模型通道:用https://taotoken.net/models网页端发一条测试消息,确认 Key 和模型可用,再排查 Java 代码。这样能把"模型问题"和"流程问题"分开定位。
6. 会签跑通之后:把摘要接进主流程,让审批人少翻页
整套方案跑通后,主流程审批人看到的界面是这样的:会签区域列出每个参与者的部门、姓名、结果、意见、时间,顶部一段模型生成的摘要。提交按钮在会签未完成时是灰的,全部签完自动恢复。前端互锁 + 后端canNext兜底,双保险。
如果你要把这套接进现有项目,建议按这个顺序落地:先建T_HJS_s和T_HJ_s两张表,再部署hq子流程定义,然后接starthq/completeS/completeP三个后端方法,最后加gethqinfo前端轮询。模型摘要可以最后接,因为它不影响流程流转,属于增强项。
模型调用这块,统一 Key 的好处是换模型成本低。会签摘要这种任务,你可以先用一个模型跑通,觉得归纳不够准,改taotoken.json里的model字段再试,Base URL 和鉴权代码一行不用动。接入文档在https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_content=doc&utm_campaign=rewrite,API Key 在https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_content=api-keys&utm_campaign=rewrite,模型调试在https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_content=models&utm_campaign=rewrite。
最后留一个实战技巧:会签意见摘要的 prompt 里,明确要求模型输出"通过人数/不通过人数/分歧点/结论"四段,比让它自由发挥稳定得多。政务场景要的是可预期,不是文采。