Skip to content

ToolCallingChatOptions support internalToolExecutionMaxIterations, to limit the maximum number of tool calls and prevent infinite recursive calls to LLM in special cases #3380

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 33 commits into
base: main
Choose a base branch
from

Conversation

lambochen
Copy link
Contributor

@lambochen lambochen commented May 29, 2025

This PR primarily addresses the issue of infinite recursive calls to LLM under certain special conditions, with internalToolExecutionEnabled=true in place, ref issue: #3333.

By introducing support for toolExecutionMaxIterations in ToolCallingChatOptions and performing call count checks in each ChatModel, interrupt the process when the maximum call count is reached.

[x] Sign the contributor license agreement
[x] Rebase your changes on the latest main branch and squash your commits
[x] Add/Update unit tests as needed
[x] Run a build and make sure all tests pass prior to submission

lambochen added 6 commits May 29, 2025 22:49
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
@lambochen lambochen marked this pull request as draft May 29, 2025 16:37
lambochen added 8 commits May 30, 2025 00:42
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
@markpollack
Copy link
Member

thanks, one comment is that we need to preserve api compatability and

public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatResponse, int attempts) {

would need to be an overload vs adding a new method parameter.

Signed-off-by: lambochen <lambochen@yeah.net>
@lambochen
Copy link
Contributor Author

thanks, one comment is that we need to preserve api compatability and

public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatResponse, int attempts) {

would need to be an overload vs adding a new method parameter.

Your suggestion is great, I'll optimize it, thanks

lambochen added 8 commits May 30, 2025 01:35
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
@lambochen lambochen marked this pull request as ready for review May 31, 2025 13:37
@lambochen
Copy link
Contributor Author

Hi, @markpollack @tzolov , the PR is ready, please review it, thank you.

If there are any issues or if additional content is needed, please to comment, and I will promptly correct or supplement it. thank you

/**
* No limit for tool execution attempts.
*/
int TOOL_EXECUTION_NO_LIMIT = Integer.MAX_VALUE;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say it's a mistake that no limit was set in the original implementation. I would consider having a lower value as the default limit. We should think of a good default value here that avoids infinite loops.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I agree with your point. To ensure downward compatibility, I have used Integer.MAX_VALUE (a very large number) to indicate no limit.

I tend to prefer setting a smaller value to limit the number of executions and the scope of impact on the program, but I don't have specific comparative data. If you have suggestions, please feel free to share them. Thank you

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tzolov what do you think? Should we keep it unbounded (i.e infinite loop) or find a good default value?

@lambochen lambochen requested a review from ThomasVitale June 1, 2025 15:14
Signed-off-by: lambochen <lambochen@yeah.net>
@lambochen
Copy link
Contributor Author

@ThomasVitale Hi, I have optimized the code and replied to your suggestion. Please re-review this PR. Thank you for your hard work.

@aichemzee
Copy link

Thank you @lambochen for picking this up. This would be a very helpful change with tool calling.

@lambochen lambochen changed the title ToolCallingChatOptions support internalToolExecutionMaxAttempts, to limit the maximum number of tool calls and prevent infinite recursive calls to LLM in special cases ToolCallingChatOptions support internalToolExecutionMaxIterations, to limit the maximum number of tool calls and prevent infinite recursive calls to LLM in special cases Jun 9, 2025
Signed-off-by: lambochen <lambochen@yeah.net>
@tzolov
Copy link
Contributor

tzolov commented Jun 20, 2025

Thanks @lambochen!

I agreed on handling the infinite loop risk, but, IMO, should consider:

  1. What should happen when max interactions are reached? Currently it silently returns the last tool response. I think we should throw an error instead - maybe ToolExecutionException?

  2. Could we introduce a ToolExecutionRequest (like ToolExecutionResult) containing the prompt, chat response, and iteration counter? We could also extend ToolExecutionResult to include the call count. This would let us centralize the check logic in executeToolCalls.

@lambochen , @ThomasVitale what do you think?

@lambochen
Copy link
Contributor Author

Thanks! @tzolov

  1. What should happen when max interactions are reached? Currently it silently returns the last tool response. I think we should throw an error instead - maybe ToolExecutionException?

I agree, should we also consider other strategies?

  1. Silently return the last tool response
  2. Throw a ToolExecutionException
  3. Custom handler
  1. Could we introduce a ToolExecutionRequest (like ToolExecutionResult) containing the prompt, chat response, and iteration counter? We could also extend ToolExecutionResult to include the call count. This would let us centralize the check logic in executeToolCalls.

That's great, introducing ToolExecutionRequest will make the logic much cleaner.

@tzolov
Copy link
Contributor

tzolov commented Jun 23, 2025

@lambochen

I agree, should we also consider other strategies?

I don't think we need this flexibility right now. I can't see a use case where "silently return the last tool response" would be helpful. Also, this change needs to work with internalToolExecutionEnabled=false, and returning a random tool response could cause issues there.

Let's save strategy support for a future PR if we find compelling reasons for it.

@ThomasVitale
Copy link
Contributor

I'll have a look tomorrow at the updated PR and share my comments then. Thanks!

@lambochen
Copy link
Contributor Author

@lambochen

I agree, should we also consider other strategies?

I don't think we need this flexibility right now. I can't see a use case where "silently return the last tool response" would be helpful. Also, this change needs to work with internalToolExecutionEnabled=false, and returning a random tool response could cause issues there.

Let's save strategy support for a future PR if we find compelling reasons for it.

@tzolov Thank you for your reply, I agree with your point, I will optimize the code and resubmit, thank you

@lambochen lambochen marked this pull request as draft June 23, 2025 15:20
…tool calling

Signed-off-by: lambochen <lambochen@yeah.net>
@lambochen
Copy link
Contributor Author

  1. Could we introduce a ToolExecutionRequest (like ToolExecutionResult) containing the prompt, chat response, and iteration counter? We could also extend ToolExecutionResult to include the call count. This would let us centralize the check logic in executeToolCalls.

Hi, @tzolov
I didn't quite understand your suggestion. Are you expecting to centralize all tool execution logic into ToolCallingManager.executeToolCalls, including isToolExecutionRequired and isLimitExceeded?
I hope you can clarify my confusion. Thank you.

…Checker#isLimitExceeded

Signed-off-by: lambochen <lambochen@yeah.net>
@lambochen lambochen marked this pull request as ready for review June 24, 2025 08:38
* @param toolExecutionIterations The number of toolExecutionIterations
* @return true if the tool execution limit has been exceeded, false otherwise
*/
default boolean isLimitExceeded(ChatOptions promptOptions, int toolExecutionIterations) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a public API or a utility method?

* @param toolExecutionIterations The number of toolExecutionIterations
* @return true if the tool execution limit has been exceeded, false otherwise
*/
default boolean isLimitExceeded(ChatOptions promptOptions, int toolExecutionIterations) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here as in ToolExecutionEligibilityChecker. Should this be a public API or a utility method?

I would suggest moving this to ToolCallingChatOptions as a static method, similar to other utilities we have there to deal with tool execution.

Copy link
Contributor Author

@lambochen lambochen Jun 25, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, @ThomasVitale ,Thank you for your comment, your considerations are very valuable.

Regarding this issue, I think both are acceptable.

As a public API, it opens up possibilities for users to extend, allowing users to be more flexible when implementing ToolExecutionEligibilityPredicate, of course, this is on the premise that users expect to implement isLimitExceeded.

As a utility method, it's also a good design that can meet the business's customization for ToolExecutionEligibilityPredicate (by implementing the isToolExecutionRequired method). It's equivalent to users only needing to be aware of the isToolExecutionRequired SPI method, which would be more concise.

What do you think?

lambochen added 3 commits July 3, 2025 19:06
Signed-off-by: lambochen <lambochen@yeah.net>
…onMaxIterations

Signed-off-by: lambochen <lambochen@yeah.net>
Signed-off-by: lambochen <lambochen@yeah.net>
@lambochen lambochen requested a review from ThomasVitale July 3, 2025 11:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants