feat(tool-bailian-kb): 统一设定 kb_search 与 kb_chat 的 agent_id 为必填参数

- 修改文档和代码,将 kb_search 和 kb_chat 工具的 agent_id 参数在 schema 中标记为必填
- 明确模型路径调用时必须显式传递 agent_id,程序化调用缺省时回退默认服务
- 定义工具参数验证时强制 agent_id 必填,防止无效调用的运行时错误
- 调整默认服务回退逻辑为防御机制,保证 schema 校验优先拒绝缺失 agent_id 的请求
- 增强错误提示信息,引导用户正确配置和使用 agent_id 参数
- 更新测试覆盖相关改动,确保 agent_id 必填规则和回退机制符合预期
- 修改说明自动解析凭证和工作空间配置的行为,用户无需手工传递这些值
- 移除已废弃的 kb_service_list 服务发现,使用 kscli
This commit is contained in:
zeyu.fz
2026-08-17 20:13:29 +08:00
parent efcb1d90ed
commit 30c2be67a0
4 changed files with 69 additions and 49 deletions
+7 -7
View File
@@ -22,8 +22,8 @@
|---|---|---|---|---|
| `DASHSCOPE_API_KEY` | —(无 settings 面) | —(无 config 面) | ✅ | 工具调用报错并引导配置 |
| `BAILIAN_WORKSPACE_ID` | ✅ `workspaceId` | ✅ `workspaceId` | ✅ | 工具调用报错并引导配置 |
| `BAILIAN_DEFAULT_RETRIEVE_AGENT_ID` | ✅ `defaultRetrieveAgentId` | ✅ `defaultRetrieveAgentId` | ✅ | `kb_search` 的 `agent_id` 参数变必填 |
| `BAILIAN_DEFAULT_CHAT_AGENT_ID` | ✅ `defaultChatAgentId` | ✅ `defaultChatAgentId` | ✅ | `kb_chat` 的 `agent_id` 参数变必填 |
| `BAILIAN_DEFAULT_RETRIEVE_AGENT_ID` | ✅ `defaultRetrieveAgentId` | ✅ `defaultRetrieveAgentId` | ✅ | `kb_search` 无 `agent_id` 的**程序化**调用报错并附配置指引(模型侧 schema 恒必填) |
| `BAILIAN_DEFAULT_CHAT_AGENT_ID` | ✅ `defaultChatAgentId` | ✅ `defaultChatAgentId` | ✅ | `kb_chat` 无 `agent_id` 的**程序化**调用报错并附配置指引(模型侧 schema 恒必填) |
行为参数(`endpointHost`/`agentVersion`/`chatTimeoutMs`)在 config/settings 层(设置文档可改,实时生效)。
@@ -59,19 +59,19 @@ Config 同时注册为 `bailian-kb` settings namespace(`installSettingsSection
|---|---|---|---|
| `workspaceId` | string? | — | 百炼工作空间 id;API host 为 workspace 子域名 `https://<workspaceId>.<endpointHost>`。未设置时每次调用回退 `BAILIAN_WORKSPACE_ID` credential |
| `endpointHost` | string | `cn-beijing.maas.aliyuncs.com` | host 后缀,其他 region/私有化时替换 |
| `defaultRetrieveAgentId` | string? | — | 默认检索服务;`kb_search` 的 `agent_id` 参数 schema 恒可选,默认值每次调用运行时解析(settings/config → credential) |
| `defaultChatAgentId` | string? | — | 默认对话服务;`kb_chat` 的 `agent_id` 参数 schema 恒可选,默认值每次调用运行时解析(settings/config → credential) |
| `defaultRetrieveAgentId` | string? | — | 默认检索服务;`kb_search` 的 `agent_id` 参数 schema **恒必填**(模型永远显式传),此默认仅作用于省略 `agent_id` 的程序化调用,每次调用运行时解析(settings/config → credential) |
| `defaultChatAgentId` | string? | — | 默认对话服务;`kb_chat` 的 `agent_id` 参数 schema **恒必填**(模型永远显式传),此默认仅作用于省略 `agent_id` 的程序化调用,每次调用运行时解析(settings/config → credential) |
| `agentVersion` | string? | — | `beta`(草稿调试)或已发布版本号;不暴露给模型 |
| `chatTimeoutMs` | number | 300000 | kb_chat 超时;服务端是分钟级 agentic loop |
凭证与回退链:`DASHSCOPE_API_KEY` 只走 `ctx.credentials` 引用(write-only,每次调用重新解析,热更换生效);`workspaceId`/`defaultRetrieveAgentId`/`defaultChatAgentId` 先取 settings 解析值(用户层 > entry config),缺失时回退同名 credential(`BAILIAN_WORKSPACE_ID`/`BAILIAN_DEFAULT_RETRIEVE_AGENT_ID`/`BAILIAN_DEFAULT_CHAT_AGENT_ID`),都没有时报错并附配置指引。
凭证与回退链:`DASHSCOPE_API_KEY` 只走 `ctx.credentials` 引用(write-only,每次调用重新解析,热更换生效);`workspaceId`/`defaultRetrieveAgentId`/`defaultChatAgentId` 先取 settings 解析值(用户层 > entry config),缺失时回退同名 credential(`BAILIAN_WORKSPACE_ID`/`BAILIAN_DEFAULT_RETRIEVE_AGENT_ID`/`BAILIAN_DEFAULT_CHAT_AGENT_ID`),都没有时报错并附配置指引。注意:`agent_id` 在两个工具的 schema 中恒必填,模型路径不会触发默认服务回退;回退保留是为程序化调用与 credential 热切换。
## 工具
| 工具 | 参数 | 返回 |
|---|---|---|
| `kb_search` | `query`、`agent_id`(见 defaultRetrieveAgentId)、`top_k?`(默认 5,**客户端截断**——服务端无此参数)、`images?` | chunks(text/score/来源)+ total |
| `kb_chat` | `message`、`agent_id`(见 defaultChatAgentId) | 完整答案(内部消费 SSE 流缓冲返回)+ request_id |
| `kb_search` | `query`、`agent_id`(**必填**;程序化省略时回退 defaultRetrieveAgentId)、`top_k?`(默认 5,**客户端截断**——服务端无此参数)、`images?` | chunks(text/score/来源)+ total |
| `kb_chat` | `message`、`agent_id`(**必填**;程序化省略时回退 defaultChatAgentId) | 完整答案(内部消费 SSE 流缓冲返回)+ request_id |
服务发现(`kb_service_list` 已移除):通过 `kscli service list` CLI 命令查询可用检索/对话服务及其 agent_id。
+3 -1
View File
@@ -21,7 +21,9 @@ export function registerSkill(ctx: Context): void {
name: 'bailian-kb-management',
description:
'Manage Bailian knowledge bases with the kscli CLI: create/update KBs, upload documents, deploy '
+ 'retrieval services, and maintain chunks. Retrieval itself uses the native kb_search/kb_chat tools.',
+ 'retrieval services, and maintain chunks. Retrieval itself uses the native kb_search/kb_chat tools. '
+ 'Credentials and workspace for kb_search/kb_chat resolve automatically from DSH config '
+ '(bailian-kb in ~/.dsh/settings.yaml, DASHSCOPE_API_KEY in ~/.dsh/.credentials.yaml).',
content,
source: 'bundled',
resourceBase: { kind: 'directory', path: SKILL_DIR },
+31 -10
View File
@@ -1,9 +1,10 @@
/**
* The two model-facing knowledge tools (kb_search, kb_chat). agent_id stays optional in the schema
* regardless of deployment: the default services (patch config or credential)
* can change at runtime through the credentials domain, so the fallback runs
* per call and a missing default surfaces as an executable error instead of a
* load-time schema difference.
* The two model-facing knowledge tools (kb_search, kb_chat). agent_id is REQUIRED in the schema:
* a model cannot know from the tool spec whether this deployment configures a default service, and a
* missing default previously only surfaced at call time, forcing a wasted round-trip. The per-call
* fallback to a configured default (settings/config or credential) is retained as defense-in-depth,
* but note defineTool validates args against the schema before execute, so through that entry point
* the fallback is inert; the model-facing contract is explicit.
*/
import { defineTool } from '@deepseek-ai/dsh-tools'
@@ -45,13 +46,22 @@ export function createKbTools(deps: KbToolDeps) {
const { client, resolveDefaultRetrieveAgentId, resolveDefaultChatAgentId } = deps
const agentIdParam = {
type: 'string' as const,
description: 'Retrieval/Q&A service id; omit to use the default service when this deployment configures one (find ids via `kscli service list`).',
required: true as const,
description: 'Retrieval/Q&A service id. REQUIRED: the schema cannot know whether this deployment '
+ 'configures a default service, so always pass one. Find ids via '
+ '`kscli service list --scene search --workspace-id <workspaceId>` (workspaceId resolves '
+ 'automatically from DSH settings: bailian-kb.workspaceId in ~/.dsh/settings.yaml).',
}
const resolveRetrieveAgentId = async (supplied: string | undefined): Promise<string> => {
if (supplied !== undefined) return supplied
const defaultId = resolveDefaultRetrieveAgentId === undefined ? undefined : await resolveDefaultRetrieveAgentId()
if (defaultId === undefined) {
throw new Error('agent_id is required: no default retrieval service is configured; discover services with `kscli service list`')
throw new Error(
'agent_id is required: no default retrieval service is configured. Pass agent_id explicitly '
+ '(find ids: `kscli service list --scene search --workspace-id <workspaceId>`), or configure a '
+ 'default: bailian-kb.defaultRetrieveAgentId in ~/.dsh/settings.yaml or '
+ 'BAILIAN_DEFAULT_RETRIEVE_AGENT_ID in ~/.dsh/.credentials.yaml.',
)
}
return defaultId
}
@@ -59,7 +69,12 @@ export function createKbTools(deps: KbToolDeps) {
if (supplied !== undefined) return supplied
const defaultId = resolveDefaultChatAgentId === undefined ? undefined : await resolveDefaultChatAgentId()
if (defaultId === undefined) {
throw new Error('agent_id is required: no default chat service is configured; discover services with `kscli service list`')
throw new Error(
'agent_id is required: no default chat service is configured. Pass agent_id explicitly '
+ '(find ids: `kscli service list --scene chat --workspace-id <workspaceId>`), or configure a '
+ 'default: bailian-kb.defaultChatAgentId in ~/.dsh/settings.yaml or '
+ 'BAILIAN_DEFAULT_CHAT_AGENT_ID in ~/.dsh/.credentials.yaml.',
)
}
return defaultId
}
@@ -71,7 +86,10 @@ export function createKbTools(deps: KbToolDeps) {
+ 'references for you to verify, cite, or combine with other context. Retrieval scope and strategy '
+ '(multi-KB weighting, routing, reranking) come from the service configuration. '
+ 'top_k caps how many chunks return (client-side cut of the score-ranked results). '
+ 'Use kb_chat instead when the user question can be answered by the knowledge base alone.',
+ 'Use kb_chat instead when the user question can be answered by the knowledge base alone. '
+ 'Credentials and workspace resolve automatically from DSH config '
+ '(bailian-kb in ~/.dsh/settings.yaml, DASHSCOPE_API_KEY in ~/.dsh/.credentials.yaml) — '
+ 'never read or pass them yourself. agent_id is REQUIRED (see its parameter description).',
parameters: {
query: { type: 'string', required: true, description: 'Search query text.' },
agent_id: agentIdParam,
@@ -139,7 +157,10 @@ export function createKbTools(deps: KbToolDeps) {
+ '(multi-round retrieval + reranking + grounded generation). For knowledge Q&A this typically outperforms '
+ 'searching and synthesizing yourself when the question can be answered by the knowledge base alone; '
+ 'use kb_search instead when you need raw chunks to verify, cite, or combine with other work. '
+ 'The pipeline runs an internal analysis/retrieval loop and may take a few minutes.',
+ 'The pipeline runs an internal analysis/retrieval loop and may take a few minutes. '
+ 'Credentials and workspace resolve automatically from DSH config '
+ '(bailian-kb in ~/.dsh/settings.yaml, DASHSCOPE_API_KEY in ~/.dsh/.credentials.yaml) — '
+ 'never read or pass them yourself. agent_id is REQUIRED (see its parameter description).',
parameters: {
message: { type: 'string', required: true, description: 'The question to ask.' },
agent_id: agentIdParam,
+28 -31
View File
@@ -36,44 +36,41 @@ describe('createKbTools', () => {
expect(body.agent_id).toBe('aid-1')
})
it('agent_id stays optional in the schema regardless of a configured default', () => {
const withoutDefault = toolsWith(vi.fn()).byName.kb_search!
const withDefault = toolsWith(vi.fn(), undefined, async () => 'aid-fixed').byName.kb_search!
it('agent_id is required in the schema for both tools', () => {
const { byName } = toolsWith(vi.fn())
// defineTool compiles the spec into JSON Schema: requiredness lives in the top-level `required` array.
const requiredList = (tool: { parameters: Record<string, unknown> }) =>
(tool.parameters.required ?? []) as string[]
// The default can arrive or leave at runtime via the credentials domain, so
// the schema cannot promise requiredness either way.
expect(requiredList(withoutDefault)).not.toContain('agent_id')
expect(requiredList(withDefault)).not.toContain('agent_id')
expect(requiredList(byName.kb_search!)).toContain('agent_id')
expect(requiredList(byName.kb_chat!)).toContain('agent_id')
})
it('kb_search falls back to the per-call default retrieve resolver as an explicit resolve step', async () => {
it('a missing agent_id is rejected by schema validation before execute (even with a default resolver)', async () => {
const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse)
const { byName } = toolsWith(postJson, undefined, async () => 'aid-fixed')
await byName.kb_search!.execute({ query: 'q' }, EXEC)
expect((postJson.mock.calls[0]![1] as Record<string, unknown>).agent_id).toBe('aid-fixed')
})
it('a missing agent_id without any default resolves to executable discovery guidance', async () => {
const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse)
const { byName } = toolsWith(postJson)
const err = await byName.kb_search!.execute({ query: 'q' }, EXEC).catch((e: unknown) => e)
expect((err as Error).message).toContain('kscli service list')
})
it('kb_search re-resolves the default per call (credential hot-swap contract)', async () => {
const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse)
let current: string | undefined
const resolveDefaultRetrieveAgentId = vi.fn(async () => current)
const resolveDefaultRetrieveAgentId = vi.fn(async () => 'aid-fixed')
const { byName } = toolsWith(postJson, undefined, resolveDefaultRetrieveAgentId)
current = 'aid-one'
await byName.kb_search!.execute({ query: 'q' }, EXEC)
current = undefined
await byName.kb_search!.execute({ query: 'q' }, EXEC).catch(() => {})
expect(resolveDefaultRetrieveAgentId).toHaveBeenCalledTimes(2)
expect((postJson.mock.calls[0]![1] as Record<string, unknown>).agent_id).toBe('aid-one')
expect(postJson).toHaveBeenCalledTimes(1)
// defineTool validates args against the compiled schema before execute runs,
// so with agent_id required the per-call default fallback is never consulted
// through this entry point; it stays as defense-in-depth only.
const err = await byName.kb_search!.execute({ query: 'q' }, EXEC).catch((e: unknown) => e)
expect((err as Error).message).toContain('agent_id')
expect(resolveDefaultRetrieveAgentId).not.toHaveBeenCalled()
expect(postJson).not.toHaveBeenCalled()
})
it('a missing agent_id without any default is also a schema rejection, not the runtime guidance error', async () => {
const { byName } = toolsWith(vi.fn())
const err = await byName.kb_search!.execute({ query: 'q' }, EXEC).catch((e: unknown) => e)
expect((err as Error).message).toContain('missing required property')
})
it('a supplied agent_id bypasses the default resolver entirely', async () => {
const postJson = vi.fn(async (_path: string, _body: unknown) => searchResponse)
const resolveDefaultRetrieveAgentId = vi.fn(async () => 'aid-default')
const { byName } = toolsWith(postJson, undefined, resolveDefaultRetrieveAgentId)
await byName.kb_search!.execute({ query: 'q', agent_id: 'aid-explicit' }, EXEC)
expect(resolveDefaultRetrieveAgentId).not.toHaveBeenCalled()
expect((postJson.mock.calls[0]![1] as Record<string, unknown>).agent_id).toBe('aid-explicit')
})
it('a 4xx failure passes the original error through unchanged', async () => {